feat: add versioned multi-component assessment registry

This commit is contained in:
Luftmensch
2026-09-04 10:33:36 +07:00
parent 4f4c7df4ed
commit 9f34cf8afd
9 changed files with 906 additions and 38 deletions
+69 -7
View File
@@ -428,6 +428,54 @@
</div>
{% if user_info and user_info.role == 'admin' %}
<div class="card" id="profile-registry">
<div class="card-header"><h2 class="card-title">🧩 Registry Category → Component → Profile → Version</h2></div>
<p style="color: var(--text-muted);">Các phiên bản là bất biến; chỉ phiên bản active được dùng khi audit. Mọi thao tác bên dưới chỉ dành cho admin.</p>
<div class="form-row">
<form class="registry-json-form" data-endpoint="{{ url_for('registry_create_category_v2') }}">
<div class="form-group"><label>Category ID</label><input name="id" placeholder="database-platforms" required></div>
<div class="form-group"><label>Tên category</label><input name="name" required></div>
<button class="btn btn-primary" type="submit">Thêm category</button>
</form>
<form class="registry-json-form" data-endpoint="{{ url_for('registry_create_component_v2') }}">
<div class="form-group"><label>Category ID</label><input name="category_id" required></div>
<div class="form-group"><label>Component ID</label><input name="id" required></div>
<div class="form-group"><label>Tên component</label><input name="name" required></div>
<button class="btn btn-primary" type="submit">Thêm component</button>
</form>
<form class="registry-json-form" data-endpoint="{{ url_for('registry_create_profile_v2') }}">
<div class="form-group"><label>Component ID</label><input name="component_id" required></div>
<div class="form-group"><label>Profile ID</label><input name="id" required></div>
<div class="form-group"><label>Tên profile</label><input name="name" required></div>
<div class="form-group"><label>Aliases (phân cách dấu phẩy)</label><input name="aliases"></div>
<button class="btn btn-primary" type="submit">Thêm profile</button>
</form>
</div>
{% for category in registry.categories %}
<h3>{{ category.name }} <code>{{ category.id }}</code></h3>
{% for component in category.components %}
<h4>{{ component.name }} <code>{{ component.id }}</code></h4>
{% for profile in component.profiles %}
<div style="border:1px solid var(--border-color);padding:14px;margin:10px 0;border-radius:8px;">
<strong>{{ profile.name }}</strong> <code>{{ profile.id }}</code>
<form action="{{ url_for('registry_upload_version_v2', profile_id=profile.id) }}" method="post" enctype="multipart/form-data" class="upload-form registry-action-form">
<div class="form-row">
<div class="form-group"><label>Version</label><input name="version" required></div>
<div class="form-group"><label>Checklist XLSX</label><input type="file" name="checklist_file" accept=".xlsx" required></div>
<div class="form-group"><label>Rule JSON</label><input type="file" name="config_file" accept=".json" required></div>
<label><input type="checkbox" name="activate" value="true"> Kích hoạt ngay</label>
</div>
<button class="btn btn-primary" type="submit">Upload version</button>
</form>
{% for version in profile.versions %}
<span style="display:inline-block;margin:5px;"><code>{{ version.version }}</code>{% if version.active %} ✅ active{% else %}
<form class="registry-action-form" style="display:inline" action="{{ url_for('registry_activate_v2', profile_id=profile.id, version=version.version) }}" method="post"><button class="btn" type="submit">Activate</button></form>{% endif %}</span>
{% endfor %}
<form class="registry-action-form" style="display:inline" action="{{ url_for('registry_rollback_v2', profile_id=profile.id) }}" method="post"><button class="btn" type="submit">Rollback</button></form>
</div>
{% endfor %}{% endfor %}{% endfor %}
</div>
<!-- Upload Checklist and Rule Configuration -->
<div class="card">
<div class="card-header">
@@ -681,14 +729,28 @@
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadForm.addEventListener(eventName, () => uploadForm.classList.add('dragover'), false);
});
if (uploadForm) {
['dragenter', 'dragover'].forEach(eventName => uploadForm.addEventListener(eventName, () => uploadForm.classList.add('dragover'), false));
['dragleave', 'drop'].forEach(eventName => uploadForm.addEventListener(eventName, () => uploadForm.classList.remove('dragover'), false));
}
['dragleave', 'drop'].forEach(eventName => {
uploadForm.addEventListener(eventName, () => uploadForm.classList.remove('dragover'), false);
});
document.querySelectorAll('.registry-json-form').forEach(form => form.addEventListener('submit', async event => {
event.preventDefault();
const body = Object.fromEntries(new FormData(form).entries());
if (body.aliases !== undefined) body.aliases = body.aliases.split(',').map(x => x.trim()).filter(Boolean);
const response = await fetch(form.dataset.endpoint, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)});
const result = await response.json();
if (!response.ok) return alert(result.detail || 'Registry update failed');
location.reload();
}));
document.querySelectorAll('.registry-action-form').forEach(form => form.addEventListener('submit', async event => {
event.preventDefault();
const response = await fetch(form.action, {method: 'POST', body: new FormData(form)});
const result = await response.json();
if (!response.ok) return alert(result.detail || 'Registry action failed');
location.reload();
}));
</script>
</body>
</html>
</html>