MCP Server
Active
1
0
Files
Organizations_register/vdvc-register-viewer.html
2026-02-05 18:44:03 +00:00

245 lines
8.8 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VDVC Register Viewer</title>
<style>
:root { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
html, body { height: 100%; }
body { margin: 0; display: flex; flex-direction: column; overflow: hidden; }
header { padding: 12px 16px; border-bottom: 1px solid #ddd; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
header .meta { margin-left: auto; opacity: 0.85; font-size: 12px; }
main { display: grid; grid-template-columns: 1fr 420px; gap: 0; flex: 1; min-height: 0; overflow: hidden; }
.panel { border-right: 1px solid #eee; display: flex; flex-direction: column; min-height: 0; }
.panel:last-child { border-right: 0; }
.toolbar { padding: 10px 12px; border-bottom: 1px solid #eee; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
button { padding: 8px 10px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer; }
button:disabled { opacity: 0.55; cursor: not-allowed; }
.status { font-size: 12px; opacity: 0.85; }
textarea {
width: 100%;
flex: 1;
min-height: 0;
border: 0;
outline: none;
padding: 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
line-height: 1.35;
white-space: pre-wrap;
word-break: break-word;
overflow: auto;
}
pre { margin: 0; padding: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.35; white-space: pre-wrap; word-break: break-word; }
.side { flex: 1; min-height: 0; overflow: auto; }
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; border: 1px solid #ddd; font-size: 12px; }
.warn { color: #b00; }
</style>
</head>
<body>
<header>
<strong>VDVC Register Viewer</strong>
<span class="pill" id="dirtyPill">clean</span>
<span class="pill" id="connPill">not connected</span>
<div class="meta" id="meta"></div>
</header>
<main>
<section class="panel">
<div class="toolbar">
<button id="btnLoad">Load</button>
<button id="btnSave" disabled>Save (commit)</button>
<button id="btnFormat">Format</button>
<button id="btnValidate">Check XML</button>
<span class="status" id="status"></span>
</div>
<textarea id="editor" spellcheck="false"></textarea>
</section>
<section class="panel">
<div class="toolbar">
<strong>Info</strong>
<span class="status" id="infoStatus"></span>
</div>
<div class="side">
<pre id="info"></pre>
</div>
</section>
</main>
<script>
const els = {
dirtyPill: document.getElementById('dirtyPill'),
connPill: document.getElementById('connPill'),
meta: document.getElementById('meta'),
status: document.getElementById('status'),
infoStatus: document.getElementById('infoStatus'),
info: document.getElementById('info'),
editor: document.getElementById('editor'),
btnLoad: document.getElementById('btnLoad'),
btnSave: document.getElementById('btnSave'),
btnFormat: document.getElementById('btnFormat'),
btnValidate: document.getElementById('btnValidate'),
};
let payload = null;
let dirty = false;
let loadedText = '';
function setDirty(v) {
dirty = v;
els.dirtyPill.textContent = dirty ? 'dirty' : 'clean';
els.btnSave.disabled = !dirty;
parent.postMessage({ type: 'PGV_DIRTY', dirty }, '*');
}
function setConnected(v) { els.connPill.textContent = v ? 'connected' : 'not connected'; }
function setStatus(msg) { els.status.textContent = msg || ''; }
function setInfoStatus(msg) { els.infoStatus.textContent = msg || ''; }
function safeJson(obj) { try { return JSON.stringify(obj, null, 2); } catch { return String(obj); } }
function formatXml(xml) {
const PADDING = ' ';
let formatted = '';
let indent = 0;
xml = xml.replace(/>\s+</g, '><').trim();
xml.split(/(?=<)/g).forEach(node => {
if (!node) return;
if (node.match(/^<\/\w/)) indent = Math.max(indent - 1, 0);
formatted += PADDING.repeat(indent) + node.trim() + '\n';
if (node.match(/^<\w([^>]*[^/])?>$/) && !node.startsWith('<?') && !node.startsWith('<!')) indent += 1;
});
return formatted.trim() + '\n';
}
function validateXml(xmlText) {
const parser = new DOMParser();
const doc = parser.parseFromString(xmlText, 'application/xml');
const err = doc.querySelector('parsererror');
return err ? (err.textContent || 'XML parse error') : null;
}
// -------- Parent fetch proxy (NO direct fetch in iframe) --------
function pgvFetch(url) {
return new Promise((resolve, reject) => {
const reqId = Math.random().toString(36).slice(2);
const onMsg = (ev) => {
const m = ev.data;
if (!m || typeof m !== 'object') return;
if (m.type !== 'PGV_FETCH_RESULT') return;
if (m.reqId !== reqId) return;
window.removeEventListener('message', onMsg);
if (m.ok) resolve(m.text || '');
else reject(new Error(m.error || 'fetch failed'));
};
window.addEventListener('message', onMsg);
parent.postMessage({ type: 'PGV_FETCH', reqId, url }, '*');
});
}
function toAbs(url) {
// payload urls are usually relative to same origin; parent will enforce allowlist
return url;
}
async function tryLoadViaApiUrl() {
if (!payload?.apiUrl) return null;
const text = await pgvFetch(toAbs(payload.apiUrl));
// api might return JSON {content:"..."}
try {
const j = JSON.parse(text);
if (j && typeof j.content === 'string') return j.content;
} catch {}
return null;
}
async function tryLoadViaRawTarget() {
const t = payload?.targets || {};
const xmlPath = t.xml || payload?.path || null;
if (!xmlPath) return null;
// If it's already a /raw/... URL in payload.targets, use it; otherwise parent can resolve it if you pass a raw URL.
// The safest is to ask for /raw/ path directly from ProcessGit; we assume parent will accept same-origin URLs.
const maybeUrl = (typeof xmlPath === 'string') ? xmlPath : null;
if (!maybeUrl) return null;
// If viewer config gave only "vdvc-register.xml", parent won't know how to map.
// So we prefer when targets.xml is a full raw URL. If it isn't, just return null.
if (!maybeUrl.includes('/raw/')) return null;
const text = await pgvFetch(toAbs(maybeUrl));
return text;
}
async function doLoad() {
setStatus('Loading...');
try {
let content = await tryLoadViaApiUrl();
if (content == null) content = await tryLoadViaRawTarget();
if (content == null) throw new Error('No usable apiUrl or raw xml target.');
els.editor.value = content;
loadedText = content;
setDirty(false);
setStatus('Loaded.');
} catch (e) {
setStatus('Load failed.');
console.error(e);
}
}
function doFormat() {
const v = els.editor.value || '';
const err = validateXml(v);
if (err) { setStatus('Cannot format: XML invalid'); return; }
els.editor.value = formatXml(v);
setDirty(els.editor.value !== loadedText);
setStatus('Formatted.');
}
function doValidate() {
const v = els.editor.value || '';
const err = validateXml(v);
if (err) { setStatus('Invalid XML'); setInfoStatus('Invalid XML'); return; }
setStatus('XML OK'); setInfoStatus('XML OK');
}
function doSave() {
setStatus('Use parent Save to commit.');
}
els.editor.addEventListener('input', () => setDirty((els.editor.value || '') !== loadedText));
els.btnLoad.addEventListener('click', doLoad);
els.btnFormat.addEventListener('click', doFormat);
els.btnValidate.addEventListener('click', doValidate);
els.btnSave.addEventListener('click', doSave);
window.addEventListener('message', (ev) => {
const msg = ev.data;
if (!msg || typeof msg !== 'object') return;
if (msg.type === 'PGV_INIT') {
payload = msg.payload || null;
setConnected(true);
els.meta.textContent = payload ? `${payload.branch || ''} ${payload.path || ''}`.trim() : '';
els.info.textContent = payload ? safeJson(payload) : 'No payload';
doLoad();
}
if (msg.type === 'PGV_COMMIT_OK') {
loadedText = els.editor.value || '';
setDirty(false);
setStatus('Committed.');
}
});
parent.postMessage({ type: 'PGV_READY' }, '*');
</script>
</body>
</html>