178 lines
7.4 KiB
HTML
178 lines
7.4 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
{% import "macros/ui.html" as m %}
|
||
|
|
|
||
|
|
{% block title %}Provisioning — {{ slug }} — Ontoref{% endblock title %}
|
||
|
|
{% block nav_provisioning %}active{% endblock nav_provisioning %}
|
||
|
|
{% block nav_group_domain %}active{% endblock nav_group_domain %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="mb-6 flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<h1 class="text-2xl font-bold">Provisioning</h1>
|
||
|
|
<p class="text-base-content/50 text-sm mt-1">Connections · Production Readiness Gates</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Tabs -->
|
||
|
|
<div role="tablist" class="tabs tabs-bordered mb-4">
|
||
|
|
<a role="tab" class="tab tab-active" onclick="switchTab('connections', this)">Connections</a>
|
||
|
|
<a role="tab" class="tab" onclick="switchTab('gates', this)">Gates</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Connections tab -->
|
||
|
|
<div id="tab-connections">
|
||
|
|
<div id="connections-content"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Gates tab -->
|
||
|
|
<div id="tab-gates" class="hidden">
|
||
|
|
<div id="gates-content" class="grid grid-cols-1 lg:grid-cols-2 gap-4"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Detail modal -->
|
||
|
|
<dialog id="detail-modal" class="modal">
|
||
|
|
<div class="modal-box w-full max-w-2xl">
|
||
|
|
<form method="dialog">
|
||
|
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-3 top-3">✕</button>
|
||
|
|
</form>
|
||
|
|
<div id="modal-content" class="text-sm space-y-3"></div>
|
||
|
|
</div>
|
||
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
||
|
|
</dialog>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const CONNECTIONS = {{ connections_json | safe }};
|
||
|
|
const GATES = {{ gates_json | safe }};
|
||
|
|
|
||
|
|
const KIND_BADGE = {
|
||
|
|
ServiceDependency: 'badge-primary',
|
||
|
|
LibraryDependency: 'badge-info',
|
||
|
|
DataDependency: 'badge-warning',
|
||
|
|
InfrastructurePeer: 'badge-ghost',
|
||
|
|
SharedOwnership: 'badge-ghost',
|
||
|
|
};
|
||
|
|
|
||
|
|
const TENSION_BADGE = {
|
||
|
|
Low: 'badge-success', Medium: 'badge-warning', High: 'badge-error',
|
||
|
|
};
|
||
|
|
|
||
|
|
function kindBadge(k) {
|
||
|
|
return `<span class="badge badge-xs font-mono ${KIND_BADGE[k] ?? 'badge-ghost'}">${k}</span>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function tensionBadge(t) {
|
||
|
|
return `<span class="badge badge-xs font-mono ${TENSION_BADGE[t] ?? 'badge-ghost'}">${t}</span>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Connections ───────────────────────────────────────────────────────────────
|
||
|
|
function renderConnections() {
|
||
|
|
const container = document.getElementById('connections-content');
|
||
|
|
const sections = [
|
||
|
|
{ key: 'upstream', label: 'Upstream', desc: 'Projects this one depends on' },
|
||
|
|
{ key: 'downstream', label: 'Downstream', desc: 'Projects that depend on this one' },
|
||
|
|
{ key: 'peers', label: 'Peers', desc: 'Sibling projects in the ecosystem' },
|
||
|
|
];
|
||
|
|
|
||
|
|
const allEmpty = sections.every(s => !(CONNECTIONS[s.key]||[]).length);
|
||
|
|
if (allEmpty) {
|
||
|
|
container.innerHTML = '<p class="text-base-content/30 text-sm py-6">No connections declared</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = sections.map(s => {
|
||
|
|
const items = CONNECTIONS[s.key] || [];
|
||
|
|
if (!items.length) return '';
|
||
|
|
return `
|
||
|
|
<div class="mb-6">
|
||
|
|
<div class="flex items-baseline gap-2 mb-2">
|
||
|
|
<h2 class="text-sm font-bold uppercase tracking-wider text-base-content/60">${s.label}</h2>
|
||
|
|
<span class="badge badge-xs badge-neutral">${items.length}</span>
|
||
|
|
</div>
|
||
|
|
<div class="overflow-x-auto">
|
||
|
|
<table class="table table-sm w-full bg-base-200 rounded-lg">
|
||
|
|
<thead>
|
||
|
|
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
|
||
|
|
<th>Project</th><th>Kind</th><th>Via</th><th>Node</th><th>Note</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
${items.map(c => `
|
||
|
|
<tr class="hover">
|
||
|
|
<td class="font-mono text-xs text-primary font-bold">
|
||
|
|
${c.url ? `<a href="${c.url}" target="_blank" class="link link-hover">${c.project}</a>` : c.project}
|
||
|
|
</td>
|
||
|
|
<td>${kindBadge(c.kind)}</td>
|
||
|
|
<td class="font-mono text-xs text-base-content/50">${c.via || '—'}</td>
|
||
|
|
<td class="font-mono text-xs text-base-content/60">${c.node || '—'}</td>
|
||
|
|
<td class="text-xs text-base-content/50">${c.note || ''}</td>
|
||
|
|
</tr>`).join('')}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Gates ─────────────────────────────────────────────────────────────────────
|
||
|
|
function renderGates() {
|
||
|
|
const container = document.getElementById('gates-content');
|
||
|
|
container.innerHTML = GATES.length === 0
|
||
|
|
? '<p class="text-base-content/30 text-sm py-6 col-span-2">No gates declared</p>'
|
||
|
|
: GATES.map((g, i) => {
|
||
|
|
const protects = (g.protege || g.protects || []);
|
||
|
|
const conditions = (g.opening_conditions || g.conditions || []);
|
||
|
|
return `
|
||
|
|
<div class="bg-base-200 rounded-lg p-4 space-y-2 cursor-pointer hover:bg-base-300 transition-colors"
|
||
|
|
onclick="showGateDetail(${i})">
|
||
|
|
<div class="flex items-start justify-between gap-2">
|
||
|
|
<div>
|
||
|
|
<p class="font-bold text-sm">${g.name || g.id}</p>
|
||
|
|
<p class="font-mono text-xs text-base-content/40">${g.id}</p>
|
||
|
|
</div>
|
||
|
|
${g.tension ? tensionBadge(g.tension) : ''}
|
||
|
|
</div>
|
||
|
|
<p class="text-xs text-base-content/60 leading-relaxed">${g.description || ''}</p>
|
||
|
|
${protects.length ? `<p class="text-xs text-base-content/40 font-mono">protects: ${protects.join(', ')}</p>` : ''}
|
||
|
|
${conditions.length ? `<p class="text-xs text-base-content/50">Conditions: ${conditions.length}</p>` : ''}
|
||
|
|
</div>`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function showGateDetail(i) {
|
||
|
|
const g = GATES[i];
|
||
|
|
if (!g) return;
|
||
|
|
const protects = (g.protege || g.protects || []);
|
||
|
|
const conditions = (g.opening_conditions || g.conditions || []);
|
||
|
|
document.getElementById('modal-content').innerHTML = `
|
||
|
|
<div class="flex items-baseline gap-2 flex-wrap">
|
||
|
|
<span class="font-mono font-bold text-primary">${g.id}</span>
|
||
|
|
${g.tension ? tensionBadge(g.tension) : ''}
|
||
|
|
</div>
|
||
|
|
<h2 class="text-lg font-bold">${g.name || g.id}</h2>
|
||
|
|
${g.description ? `<p class="text-base-content/70 text-sm leading-relaxed">${g.description}</p>` : ''}
|
||
|
|
${protects.length ? `<p class="text-xs text-base-content/50 font-mono">protects: ${protects.join(', ')}</p>` : ''}
|
||
|
|
${conditions.length ? `
|
||
|
|
<div>
|
||
|
|
<p class="text-xs font-semibold uppercase tracking-wider text-base-content/50 mb-1">Opening conditions</p>
|
||
|
|
<ul class="text-xs text-base-content/60 space-y-1 pl-3">
|
||
|
|
${conditions.map(c => `<li class="list-disc">${typeof c === 'string' ? c : JSON.stringify(c)}</li>`).join('')}
|
||
|
|
</ul>
|
||
|
|
</div>` : ''}
|
||
|
|
`;
|
||
|
|
document.getElementById('detail-modal').showModal();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Tabs ──────────────────────────────────────────────────────────────────────
|
||
|
|
function switchTab(name, el) {
|
||
|
|
['connections','gates'].forEach(t => {
|
||
|
|
document.getElementById('tab-' + t).classList.toggle('hidden', t !== name);
|
||
|
|
});
|
||
|
|
document.querySelectorAll('[role="tab"]').forEach(t => t.classList.remove('tab-active'));
|
||
|
|
el.classList.add('tab-active');
|
||
|
|
}
|
||
|
|
|
||
|
|
renderConnections();
|
||
|
|
renderGates();
|
||
|
|
</script>
|
||
|
|
{% endblock content %}
|