// ==UserScript==
// @name PPAC Known Issues Enhanced
// @namespace https://niiranen.info/
// @version 2.2
// @description Better UI for Power Platform Admin Center Known Issues
// @author Jukka Niiranen
// @match https://admin.powerplatform.microsoft.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
let isKnownIssuesPage = false;
let capturedIssues = [];
let enhancedUI = null;
let watchedIssues = JSON.parse(localStorage.getItem('ppac-watched') || '[]');
let panelPosition = localStorage.getItem('ppac-panel-position') || 'right';
// Intercept fetch
const originalFetch = window.fetch;
window.fetch = function(input, init) {
const url = typeof input === 'string' ? input : input.url;
return originalFetch.apply(this, arguments).then(response => {
if (url && url.indexOf('knownissue/search') > -1) {
response.clone().json().then(data => {
if (Array.isArray(data) && data.length > 0) {
console.log('[PPAC Enhanced] Captured', data.length, 'issues');
capturedIssues = data;
if (enhancedUI) enhancedUI.updateData(data);
}
}).catch(e => {});
}
return response;
});
};
// Intercept XHR
const origXHROpen = XMLHttpRequest.prototype.open;
const origXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this._ppacUrl = url;
return origXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
const xhr = this;
if (xhr._ppacUrl && xhr._ppacUrl.indexOf('knownissue/search') > -1) {
xhr.addEventListener('load', function() {
try {
const data = JSON.parse(xhr.responseText);
if (Array.isArray(data) && data.length > 0) {
capturedIssues = data;
if (enhancedUI) enhancedUI.updateData(data);
}
} catch (e) {}
});
}
return origXHRSend.apply(this, arguments);
};
// Navigation detection
function checkAndInject() {
if (location.href.indexOf('knownIssues') > -1 && !isKnownIssuesPage) {
isKnownIssuesPage = true;
setTimeout(injectEnhancedUI, 1500);
} else if (location.href.indexOf('knownIssues') === -1) {
isKnownIssuesPage = false;
if (enhancedUI) { enhancedUI.destroy(); enhancedUI = null; }
}
}
const pushState = history.pushState;
history.pushState = function() {
pushState.apply(history, arguments);
setTimeout(checkAndInject, 100);
};
window.addEventListener('popstate', () => setTimeout(checkAndInject, 100));
window.addEventListener('load', () => setTimeout(checkAndInject, 500));
function injectEnhancedUI() {
if (document.getElementById('ppac-enhanced-root')) return;
console.log('[PPAC Enhanced] Injecting UI v2.2');
const root = document.createElement('div');
root.id = 'ppac-enhanced-root';
const styles = document.createElement('style');
styles.id = 'ppac-enhanced-styles';
styles.textContent = `
#ppac-enhanced-root {
position: fixed;
bottom: 20px;
z-index: 999999;
font-family: 'Segoe UI', sans-serif;
}
#ppac-enhanced-root.pos-right { right: 20px; }
#ppac-enhanced-root.pos-left { left: 20px; }
#ppac-toggle-btn {
background: linear-gradient(135deg, #0078d4, #106ebe);
color: white; border: none; padding: 10px 16px; border-radius: 8px;
cursor: pointer; font-size: 13px; font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
display: flex; align-items: center; gap: 6px;
}
#ppac-toggle-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.25); }
#ppac-toggle-btn.has-new { animation: pulse 2s infinite; }
@keyframes pulse { 0%, 100% { box-shadow: 0 4px 12px rgba(0,0,0,0.2); } 50% { box-shadow: 0 4px 20px rgba(0,120,212,0.5); } }
#ppac-panel {
display: none;
position: fixed;
top: 0;
width: 520px;
height: 100vh;
background: #fff;
box-shadow: 0 0 20px rgba(0,0,0,0.15);
flex-direction: column;
z-index: 999998;
}
#ppac-panel.open { display: flex; }
#ppac-panel.pos-right { right: 0; box-shadow: -4px 0 20px rgba(0,0,0,0.15); }
#ppac-panel.pos-left { left: 0; box-shadow: 4px 0 20px rgba(0,0,0,0.15); }
#ppac-panel-header {
background: #0078d4; color: white; padding: 10px 14px;
display: flex; justify-content: space-between; align-items: center;
}
#ppac-panel-header h2 { margin: 0; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
.ppac-header-btns { display: flex; gap: 2px; }
.ppac-header-btn {
background: rgba(255,255,255,0.15); border: none; color: white;
width: 26px; height: 26px; border-radius: 4px; cursor: pointer;
font-size: 14px; display: flex; align-items: center; justify-content: center;
}
.ppac-header-btn:hover { background: rgba(255,255,255,0.25); }
.ppac-header-btn.active { background: rgba(255,255,255,0.3); }
#ppac-hint-bar {
background: #fff4ce; padding: 6px 12px; font-size: 11px; color: #6a5700;
border-bottom: 1px solid #ffe69c; display: flex; align-items: center; gap: 6px;
}
#ppac-hint-bar button {
background: #0078d4; color: white; border: none; padding: 3px 8px;
border-radius: 3px; font-size: 10px; cursor: pointer;
}
#ppac-stats-bar {
background: #f8f8f8; padding: 6px 12px; border-bottom: 1px solid #e1e1e1;
display: flex; gap: 12px; font-size: 11px; color: #605e5c; flex-wrap: wrap;
}
.ppac-stat { display: flex; align-items: center; gap: 3px; }
.ppac-stat-val { font-weight: 600; color: #323130; }
#ppac-toolbar {
padding: 8px 10px; border-bottom: 1px solid #e1e1e1;
display: flex; gap: 5px; flex-wrap: wrap; align-items: center; background: #fafafa;
}
#ppac-search {
flex: 1; min-width: 100px; padding: 6px 8px;
border: 1px solid #e1e1e1; border-radius: 4px; font-size: 12px;
}
#ppac-toolbar select {
padding: 5px 4px; border: 1px solid #e1e1e1; border-radius: 4px;
font-size: 11px; background: white;
}
#ppac-toolbar button {
padding: 5px 8px; border: 1px solid #e1e1e1; border-radius: 4px;
font-size: 11px; background: white; cursor: pointer;
}
#ppac-toolbar button:hover { background: #f0f0f0; }
#ppac-filter-count { font-size: 10px; color: #605e5c; margin-left: auto; }
#ppac-list { flex: 1; overflow-y: auto; padding: 8px 10px; }
.ppac-issue {
background: #fff; border: 1px solid #e1e1e1; border-radius: 5px;
padding: 8px 10px; margin-bottom: 5px; cursor: pointer; transition: all 0.15s;
}
.ppac-issue:hover { border-color: #0078d4; box-shadow: 0 2px 6px rgba(0,120,212,0.1); }
.ppac-issue.selected { border-color: #0078d4; background: #f0f7ff; }
.ppac-issue.watched { border-left: 3px solid #ffb900; }
.ppac-issue-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 6px; margin-bottom: 4px; }
.ppac-issue-title { font-weight: 600; font-size: 12px; color: #323130; line-height: 1.3; flex: 1; }
.ppac-badges { display: flex; gap: 3px; flex-shrink: 0; }
.ppac-badge { font-size: 9px; padding: 2px 5px; border-radius: 8px; font-weight: 600; white-space: nowrap; }
.ppac-badge-active { background: #dff6dd; color: #107c10; }
.ppac-badge-resolved { background: #e8daef; color: #8764b8; }
.ppac-badge-new { background: #fff4ce; color: #8a6900; }
.ppac-badge-updated { background: #e6f2ff; color: #0078d4; }
.ppac-issue-meta { display: flex; gap: 8px; font-size: 10px; color: #605e5c; flex-wrap: wrap; align-items: center; }
.ppac-issue-product { background: #f0f0f0; padding: 1px 4px; border-radius: 2px; }
.ppac-no-solution { color: #c19c00; }
.ppac-watch-btn { background: none; border: none; cursor: pointer; font-size: 12px; padding: 0; opacity: 0.4; }
.ppac-watch-btn:hover, .ppac-watch-btn.watched { opacity: 1; }
#ppac-detail {
display: none; position: fixed; top: 0; width: 420px; height: 100vh;
background: #fff; box-shadow: 0 0 16px rgba(0,0,0,0.1); z-index: 999997; flex-direction: column;
}
#ppac-detail.open { display: flex; }
#ppac-detail.pos-right { right: 520px; box-shadow: -4px 0 16px rgba(0,0,0,0.1); }
#ppac-detail.pos-left { left: 520px; box-shadow: 4px 0 16px rgba(0,0,0,0.1); }
#ppac-detail-header { padding: 12px 14px; border-bottom: 1px solid #e1e1e1; background: #fafafa; }
#ppac-detail-close { float: right; background: none; border: none; font-size: 20px; cursor: pointer; color: #605e5c; }
#ppac-detail-title { font-size: 13px; font-weight: 600; margin-bottom: 8px; padding-right: 28px; color: #323130; line-height: 1.4; }
#ppac-detail-meta { display: flex; flex-wrap: wrap; gap: 6px; font-size: 11px; align-items: center; }
.ppac-meta-item { display: flex; gap: 3px; }
.ppac-meta-item label { color: #605e5c; }
#ppac-detail-actions { display: flex; gap: 5px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #e1e1e1; flex-wrap: wrap; }
#ppac-detail-actions button {
padding: 5px 8px; border-radius: 4px; font-size: 11px; cursor: pointer;
display: flex; align-items: center; gap: 4px;
}
.ppac-btn-primary { background: #0078d4; color: white; border: none; }
.ppac-btn-primary:hover { background: #106ebe; }
.ppac-btn-secondary { background: white; border: 1px solid #e1e1e1; color: #323130; }
.ppac-btn-secondary:hover { background: #f5f5f5; }
#ppac-detail-body { flex: 1; overflow-y: auto; padding: 12px; }
.ppac-section { margin-bottom: 12px; }
.ppac-section h3 { font-size: 10px; color: #605e5c; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; font-weight: 600; }
.ppac-section-content { font-size: 12px; line-height: 1.5; color: #323130; }
.ppac-section-content p { margin-bottom: 6px; }
.ppac-empty { text-align: center; padding: 30px 16px; color: #605e5c; font-size: 12px; }
.ppac-toast {
position: fixed; bottom: 80px; background: #323130; color: white;
padding: 8px 14px; border-radius: 6px; font-size: 12px; z-index: 1000000;
animation: fadeInOut 2s ease-in-out;
}
.ppac-toast.pos-right { right: 30px; }
.ppac-toast.pos-left { left: 30px; }
@keyframes fadeInOut { 0% { opacity: 0; } 15% { opacity: 1; } 85% { opacity: 1; } 100% { opacity: 0; } }
`;
document.head.appendChild(styles);
root.innerHTML = `
💡 Use PPAC filters on the ${panelPosition === 'right' ? 'left' : 'right'} to load data. Panel auto-captures results.
Active: 0
Resolved: 0
Has Fix: 0
⭐: 0
Waiting for data...
Use PPAC's Product/Status filters to load issues.
Results will appear here automatically.
`;
document.body.appendChild(root);
// State
let filteredIssues = [];
let selectedIssue = null;
let selectedIndex = -1;
let hintHidden = localStorage.getItem('ppac-hint-hidden') === 'true';
// Elements
const toggleBtn = document.getElementById('ppac-toggle-btn');
const closeBtn = document.getElementById('ppac-close-btn');
const minimizeBtn = document.getElementById('ppac-minimize-btn');
const positionBtn = document.getElementById('ppac-position-btn');
const panel = document.getElementById('ppac-panel');
const detail = document.getElementById('ppac-detail');
const detailClose = document.getElementById('ppac-detail-close');
const searchInput = document.getElementById('ppac-search');
const statusFilter = document.getElementById('ppac-status-filter');
const solutionFilter = document.getElementById('ppac-solution-filter');
const dateFilter = document.getElementById('ppac-date-filter');
const sortSelect = document.getElementById('ppac-sort');
const specialFilter = document.getElementById('ppac-special-filter');
const exportBtn = document.getElementById('ppac-export-btn');
const hintBar = document.getElementById('ppac-hint-bar');
const hideHintBtn = document.getElementById('ppac-hide-hint');
// Apply initial position
function applyPosition() {
root.className = 'pos-' + panelPosition;
panel.className = panel.classList.contains('open') ? 'open pos-' + panelPosition : 'pos-' + panelPosition;
detail.className = detail.classList.contains('open') ? 'open pos-' + panelPosition : 'pos-' + panelPosition;
// Update hint text
if (hintBar) {
hintBar.innerHTML = `💡 Use PPAC filters on the ${panelPosition === 'right' ? 'left' : 'right'} to load data. Panel auto-captures results. `;
document.getElementById('ppac-hide-hint').onclick = () => {
hintBar.style.display = 'none';
localStorage.setItem('ppac-hint-hidden', 'true');
};
}
}
applyPosition();
// Hide hint if previously dismissed
if (hintHidden) {
hintBar.style.display = 'none';
}
hideHintBtn.onclick = () => {
hintBar.style.display = 'none';
localStorage.setItem('ppac-hint-hidden', 'true');
};
// Panel controls
toggleBtn.onclick = () => {
panel.classList.add('open');
applyPosition();
};
closeBtn.onclick = closePanel;
minimizeBtn.onclick = closePanel;
positionBtn.onclick = () => {
panelPosition = panelPosition === 'right' ? 'left' : 'right';
localStorage.setItem('ppac-panel-position', panelPosition);
applyPosition();
showToast('Panel moved to ' + panelPosition);
};
detailClose.onclick = closeDetail;
function closePanel() {
panel.classList.remove('open');
detail.classList.remove('open');
}
function closeDetail() {
detail.classList.remove('open');
selectedIssue = null;
selectedIndex = -1;
renderList();
}
// Filters
searchInput.oninput = debounce(applyFilters, 200);
statusFilter.onchange = applyFilters;
solutionFilter.onchange = applyFilters;
dateFilter.onchange = applyFilters;
sortSelect.onchange = applyFilters;
specialFilter.onchange = applyFilters;
exportBtn.onclick = () => {
const json = JSON.stringify(capturedIssues, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'ppac-issues-' + new Date().toISOString().slice(0, 10) + '.json';
a.click();
showToast('Exported ' + capturedIssues.length + ' issues');
};
// Keyboard
document.addEventListener('keydown', (e) => {
// Ctrl+Shift+K to toggle
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'k') {
e.preventDefault();
if (panel.classList.contains('open')) closePanel();
else { panel.classList.add('open'); applyPosition(); }
return;
}
// P to toggle position (when panel open and not in input)
if (e.key.toLowerCase() === 'p' && panel.classList.contains('open') && !e.target.matches('input, select, textarea')) {
positionBtn.click();
return;
}
if (!panel.classList.contains('open')) return;
if (e.key === 'Escape') {
if (detail.classList.contains('open')) closeDetail();
else closePanel();
} else if (e.key === 'ArrowDown' && filteredIssues.length > 0 && !e.target.matches('input, select')) {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, filteredIssues.length - 1);
showDetail(filteredIssues[selectedIndex].workItemId);
scrollToSelected();
} else if (e.key === 'ArrowUp' && filteredIssues.length > 0 && !e.target.matches('input, select')) {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
showDetail(filteredIssues[selectedIndex].workItemId);
scrollToSelected();
} else if (e.key === 'Enter' && selectedIssue && !e.target.matches('input, select')) {
window.open(getPPACUrl(selectedIssue.workItemId), '_blank');
}
});
function scrollToSelected() {
const el = document.querySelector(`.ppac-issue[data-id="${filteredIssues[selectedIndex]?.workItemId}"]`);
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
function getPPACUrl(id) {
return 'https://admin.powerplatform.microsoft.com/support/knownissues/' + id;
}
function isNew(issue) {
const created = new Date(issue.createdDate);
const daysAgo = (Date.now() - created) / (1000 * 60 * 60 * 24);
return daysAgo <= 7;
}
function wasUpdated(issue) {
const created = new Date(issue.createdDate);
const changed = new Date(issue.changedDate);
return (changed - created) > (1000 * 60 * 60);
}
function hasSolution(issue) {
return issue.solution && issue.solution.trim().length > 0;
}
function isWatched(id) {
return watchedIssues.includes(id);
}
function toggleWatch(id) {
if (isWatched(id)) {
watchedIssues = watchedIssues.filter(x => x !== id);
showToast('Removed from watchlist');
} else {
watchedIssues.push(id);
showToast('Added to watchlist');
}
localStorage.setItem('ppac-watched', JSON.stringify(watchedIssues));
updateStats();
renderList();
if (selectedIssue && selectedIssue.workItemId === id) {
showDetail(id);
}
}
function showToast(msg) {
const toast = document.createElement('div');
toast.className = 'ppac-toast pos-' + panelPosition;
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
}
function updateStats() {
const active = capturedIssues.filter(i => i.state === 'Active').length;
const resolved = capturedIssues.filter(i => i.state === 'Resolved').length;
const withSolution = capturedIssues.filter(i => hasSolution(i)).length;
const watched = watchedIssues.filter(id => capturedIssues.some(i => i.workItemId === id)).length;
document.getElementById('ppac-stat-active').textContent = active;
document.getElementById('ppac-stat-resolved').textContent = resolved;
document.getElementById('ppac-stat-solution').textContent = withSolution;
document.getElementById('ppac-stat-watched').textContent = watched;
const hasNewIssues = capturedIssues.some(i => isNew(i));
toggleBtn.classList.toggle('has-new', hasNewIssues);
}
function applyFilters() {
const q = searchInput.value.toLowerCase();
const status = statusFilter.value;
const solution = solutionFilter.value;
const days = dateFilter.value ? parseInt(dateFilter.value) : 0;
const sort = sortSelect.value;
const special = specialFilter.value;
const cutoff = days ? Date.now() - (days * 24 * 60 * 60 * 1000) : 0;
filteredIssues = capturedIssues.filter(i => {
if (status && i.state !== status) return false;
if (solution === 'yes' && !hasSolution(i)) return false;
if (solution === 'no' && hasSolution(i)) return false;
if (cutoff && new Date(i.createdDate) < cutoff) return false;
if (special === 'watched' && !isWatched(i.workItemId)) return false;
if (special === 'new' && !isNew(i)) return false;
if (q) {
const text = [i.title, i.workItemId, i.product, stripHtml(i.description), stripHtml(i.solution)].join(' ').toLowerCase();
if (text.indexOf(q) < 0) return false;
}
return true;
});
filteredIssues.sort((a, b) => {
switch (sort) {
case 'created-desc': return new Date(b.createdDate) - new Date(a.createdDate);
case 'changed-desc': return new Date(b.changedDate) - new Date(a.changedDate);
case 'title-asc': return a.title.localeCompare(b.title);
default: return 0;
}
});
document.getElementById('ppac-filter-count').textContent = filteredIssues.length + '/' + capturedIssues.length;
selectedIndex = -1;
renderList();
}
function renderList() {
const container = document.getElementById('ppac-list');
if (!filteredIssues.length) {
container.innerHTML = '' + (capturedIssues.length ? 'No issues match filters' : 'Waiting for data...
Use PPAC filters to load issues.') + '
';
return;
}
container.innerHTML = filteredIssues.map((i, idx) => {
const sel = selectedIssue && selectedIssue.workItemId === i.workItemId ? ' selected' : '';
const watched = isWatched(i.workItemId) ? ' watched' : '';
const statusBadge = i.state === 'Active' ? 'ppac-badge-active' : 'ppac-badge-resolved';
const noSol = !hasSolution(i) ? '⚠️' : '';
const newBadge = isNew(i) ? 'NEW' : '';
const updatedBadge = wasUpdated(i) && !isNew(i) ? 'Upd' : '';
const watchIcon = isWatched(i.workItemId) ? '⭐' : '☆';
return `
${escHtml(i.product)}
${i.workItemId}
${fmtDate(i.createdDate)}
${noSol}
`;
}).join('');
container.querySelectorAll('.ppac-issue').forEach(el => {
el.onclick = (e) => {
if (e.target.classList.contains('ppac-watch-btn')) return;
selectedIndex = parseInt(el.dataset.idx);
showDetail(el.dataset.id);
};
});
container.querySelectorAll('.ppac-watch-btn').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
toggleWatch(btn.dataset.watch);
};
});
}
function showDetail(id) {
selectedIssue = capturedIssues.find(i => i.workItemId === id);
if (!selectedIssue) return;
renderList();
document.getElementById('ppac-detail-title').textContent = selectedIssue.title;
const statusBadge = selectedIssue.state === 'Active' ? 'ppac-badge-active' : 'ppac-badge-resolved';
const newBadge = isNew(selectedIssue) ? 'NEW' : '';
document.getElementById('ppac-detail-meta').innerHTML = `
${selectedIssue.state}
${newBadge}
${escHtml(selectedIssue.product)}
${selectedIssue.workItemId}
${fmtDate(selectedIssue.createdDate)}
${fmtDate(selectedIssue.changedDate)}
`;
const ppacUrl = getPPACUrl(selectedIssue.workItemId);
const watchLabel = isWatched(selectedIssue.workItemId) ? '⭐ Unwatch' : '☆ Watch';
document.getElementById('ppac-detail-actions').innerHTML = `
`;
document.getElementById('ppac-detail-watch').onclick = () => toggleWatch(selectedIssue.workItemId);
let body = `
Description
${selectedIssue.description || 'No description'}
`;
if (hasSolution(selectedIssue)) {
body += `
Solution / Workaround
${selectedIssue.solution}
`;
} else {
body += `
Solution / Workaround
⚠️ No solution provided yet.
`;
}
document.getElementById('ppac-detail-body').innerHTML = body;
detail.classList.add('open');
applyPosition();
}
function stripHtml(html) {
if (!html) return '';
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || '';
}
function escHtml(s) {
if (!s) return '';
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function fmtDate(d) {
if (!d) return 'N/A';
return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
function debounce(fn, ms) {
let timer;
return function() { clearTimeout(timer); timer = setTimeout(fn, ms); };
}
// API
enhancedUI = {
updateData: function(data) {
capturedIssues = data;
document.getElementById('ppac-count').textContent = '(' + data.length + ')';
updateStats();
applyFilters();
},
destroy: function() {
root.remove();
styles.remove();
}
};
if (capturedIssues.length > 0) {
enhancedUI.updateData(capturedIssues);
}
}
})();