198 lines
8.8 KiB
HTML
198 lines
8.8 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
{% block title %}Config — {{ slug }} — Ontoref{% endblock title %}
|
||
|
|
{% block nav_config %}active{% endblock nav_config %}
|
||
|
|
{% block nav_group_dev %}active{% endblock nav_group_dev %}
|
||
|
|
|
||
|
|
{% block head %}
|
||
|
|
<style>
|
||
|
|
.status-ok { @apply badge badge-success badge-xs font-mono; }
|
||
|
|
.status-warning { @apply badge badge-warning badge-xs font-mono; }
|
||
|
|
.status-error { @apply badge badge-error badge-xs font-mono; }
|
||
|
|
.kind-ruststruct { @apply badge badge-ghost badge-xs font-mono text-orange-400; }
|
||
|
|
.kind-nuscript { @apply badge badge-ghost badge-xs font-mono text-cyan-400; }
|
||
|
|
.kind-cipipeline { @apply badge badge-ghost badge-xs font-mono text-purple-400; }
|
||
|
|
.kind-external { @apply badge badge-ghost badge-xs font-mono text-yellow-400; }
|
||
|
|
</style>
|
||
|
|
{% endblock head %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="mb-6 flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<h1 class="text-2xl font-bold">Config Surface</h1>
|
||
|
|
<p class="text-base-content/50 text-sm mt-1">
|
||
|
|
<span class="font-mono">{{ config_root }}</span>
|
||
|
|
<span class="mx-1">·</span>
|
||
|
|
<span class="font-mono">{{ entry_point }}</span>
|
||
|
|
<span class="mx-1">·</span>
|
||
|
|
<span class="badge badge-neutral badge-xs font-mono">{{ kind }}</span>
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<span class="badge badge-lg {% if overall_status == 'Ok' %}badge-success{% elif overall_status == 'Warning' %}badge-warning{% else %}badge-error{% endif %}">
|
||
|
|
{{ overall_status }}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{% if not has_config_surface %}
|
||
|
|
<div class="flex flex-col items-center justify-center py-16 text-base-content/40 text-sm">
|
||
|
|
<p>No <code class="font-mono">config_surface</code> in <code class="font-mono">.ontology/manifest.ncl</code>.</p>
|
||
|
|
<p class="mt-2">Add a <code class="font-mono">config_surface</code> field to enable config management.</p>
|
||
|
|
</div>
|
||
|
|
{% else %}
|
||
|
|
|
||
|
|
{% for section in sections %}
|
||
|
|
<div class="card bg-base-200 rounded-lg mb-4">
|
||
|
|
<div class="card-body p-4">
|
||
|
|
<!-- Section header -->
|
||
|
|
<div class="flex items-start justify-between gap-2 mb-3">
|
||
|
|
<div class="flex-1">
|
||
|
|
<div class="flex items-center gap-2 flex-wrap">
|
||
|
|
<h2 class="font-bold font-mono text-lg">{{ section.id }}</h2>
|
||
|
|
{% if section.coherence %}
|
||
|
|
{% set coh = section.coherence %}
|
||
|
|
<span class="status-{{ coh.status | lower }}">{{ coh.status }}</span>
|
||
|
|
{% endif %}
|
||
|
|
{% if not section.mutable %}
|
||
|
|
<span class="badge badge-ghost badge-xs">read-only</span>
|
||
|
|
{% endif %}
|
||
|
|
</div>
|
||
|
|
{% if section.description %}
|
||
|
|
<p class="text-sm text-base-content/70 mt-0.5">{{ section.description }}</p>
|
||
|
|
{% endif %}
|
||
|
|
{% if section.rationale %}
|
||
|
|
<details class="mt-1">
|
||
|
|
<summary class="text-xs text-base-content/50 cursor-pointer hover:text-base-content/80">Why</summary>
|
||
|
|
<p class="text-xs text-base-content/60 mt-1 pl-2 border-l-2 border-base-300">{{ section.rationale }}</p>
|
||
|
|
</details>
|
||
|
|
{% endif %}
|
||
|
|
</div>
|
||
|
|
{% if section.mutable and current_role == "admin" %}
|
||
|
|
<button class="btn btn-xs btn-outline btn-primary"
|
||
|
|
onclick="openEditModal('{{ section.id }}', {{ section.current_values | json_encode() }})">
|
||
|
|
Edit
|
||
|
|
</button>
|
||
|
|
{% endif %}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Consumers -->
|
||
|
|
{% if section.consumers %}
|
||
|
|
<div class="flex flex-wrap gap-1.5 mb-3">
|
||
|
|
{% for c in section.consumers %}
|
||
|
|
<span class="kind-{{ c.kind | lower | replace(from='ruststruct', to='ruststruct') }}" title="{{ c.ref }}">
|
||
|
|
{{ c.kind | replace(from='RustStruct', to='Rust') | replace(from='NuScript', to='Nu') | replace(from='CiPipeline', to='CI') | replace(from='External', to='Ext') }}:{{ c.id }}
|
||
|
|
</span>
|
||
|
|
{% endfor %}
|
||
|
|
</div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
<!-- Unclaimed fields warning -->
|
||
|
|
{% if section.coherence and section.coherence.unclaimed_fields %}
|
||
|
|
<div class="alert alert-warning py-2 px-3 text-xs mb-3">
|
||
|
|
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||
|
|
</svg>
|
||
|
|
<span>Unclaimed fields: <span class="font-mono">{{ section.coherence.unclaimed_fields | join(sep=", ") }}</span></span>
|
||
|
|
</div>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
<!-- Current values -->
|
||
|
|
{% if section.current_values %}
|
||
|
|
<details class="text-xs">
|
||
|
|
<summary class="cursor-pointer text-base-content/50 hover:text-base-content/80 mb-1">
|
||
|
|
Current values
|
||
|
|
<span class="badge badge-ghost badge-xs ml-1">{{ section.file }}</span>
|
||
|
|
</summary>
|
||
|
|
<pre class="bg-base-300 p-3 rounded overflow-x-auto text-xs mt-2">{{ section.current_values | json_encode(pretty=true) }}</pre>
|
||
|
|
</details>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
<!-- Override history -->
|
||
|
|
{% if section.overrides and section.overrides | length > 0 %}
|
||
|
|
<details class="text-xs mt-2">
|
||
|
|
<summary class="cursor-pointer text-base-content/50 hover:text-base-content/80">
|
||
|
|
Override history <span class="badge badge-warning badge-xs ml-1">{{ section.overrides | length }}</span>
|
||
|
|
</summary>
|
||
|
|
<div class="mt-2 space-y-1">
|
||
|
|
{% for o in section.overrides %}
|
||
|
|
<div class="bg-base-300 p-2 rounded text-xs">
|
||
|
|
<span class="font-mono text-warning">{{ o.field }}</span>
|
||
|
|
<span class="text-base-content/50 mx-1">{{ o.from }} → {{ o.to }}</span>
|
||
|
|
{% if o.reason %}<span class="text-base-content/60">— {{ o.reason }}</span>{% endif %}
|
||
|
|
{% if o.ts %}<span class="text-base-content/40 ml-2">{{ o.ts }}</span>{% endif %}
|
||
|
|
</div>
|
||
|
|
{% endfor %}
|
||
|
|
</div>
|
||
|
|
</details>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{% endfor %}
|
||
|
|
|
||
|
|
<!-- Edit modal -->
|
||
|
|
<dialog id="edit-modal" class="modal">
|
||
|
|
<div class="modal-box w-11/12 max-w-2xl">
|
||
|
|
<h3 class="font-bold text-lg mb-4">Edit config section: <span id="edit-section-id" class="font-mono text-primary"></span></h3>
|
||
|
|
<div class="form-control mb-4">
|
||
|
|
<label class="label"><span class="label-text text-xs">Values (JSON)</span></label>
|
||
|
|
<textarea id="edit-values" class="textarea textarea-bordered font-mono text-sm h-48 resize-none" placeholder='{"key": "value"}'></textarea>
|
||
|
|
</div>
|
||
|
|
<div class="form-control mb-4">
|
||
|
|
<label class="label"><span class="label-text text-xs">Reason for change</span></label>
|
||
|
|
<input type="text" id="edit-reason" class="input input-bordered input-sm" placeholder="e.g. port conflict with another service">
|
||
|
|
</div>
|
||
|
|
<div class="flex gap-2 items-center mb-4">
|
||
|
|
<input type="checkbox" id="edit-dry-run" class="checkbox checkbox-sm" checked>
|
||
|
|
<label for="edit-dry-run" class="text-sm">Dry run (preview only)</label>
|
||
|
|
</div>
|
||
|
|
<div id="edit-result" class="hidden">
|
||
|
|
<div class="divider text-xs">Preview</div>
|
||
|
|
<pre id="edit-result-body" class="bg-base-300 p-3 rounded text-xs overflow-x-auto max-h-48"></pre>
|
||
|
|
</div>
|
||
|
|
<div class="modal-action">
|
||
|
|
<button class="btn btn-ghost btn-sm" onclick="document.getElementById('edit-modal').close()">Cancel</button>
|
||
|
|
<button class="btn btn-outline btn-sm" onclick="submitConfig(true)">Preview</button>
|
||
|
|
<button class="btn btn-primary btn-sm" id="apply-btn" disabled onclick="submitConfig(false)">Apply</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</dialog>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
let _editSection = '';
|
||
|
|
function openEditModal(id, values) {
|
||
|
|
_editSection = id;
|
||
|
|
document.getElementById('edit-section-id').textContent = id;
|
||
|
|
document.getElementById('edit-values').value = JSON.stringify(values, null, 2);
|
||
|
|
document.getElementById('edit-reason').value = '';
|
||
|
|
document.getElementById('edit-dry-run').checked = true;
|
||
|
|
document.getElementById('edit-result').classList.add('hidden');
|
||
|
|
document.getElementById('apply-btn').disabled = true;
|
||
|
|
document.getElementById('edit-modal').showModal();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function submitConfig(dryRun) {
|
||
|
|
let values;
|
||
|
|
try { values = JSON.parse(document.getElementById('edit-values').value); }
|
||
|
|
catch(e) { alert('Invalid JSON: ' + e.message); return; }
|
||
|
|
const reason = document.getElementById('edit-reason').value;
|
||
|
|
const body = { values, dry_run: dryRun, reason };
|
||
|
|
const res = await fetch(`/api/projects/{{ slug }}/config/${_editSection}`, {
|
||
|
|
method: 'PUT',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(body),
|
||
|
|
});
|
||
|
|
const data = await res.json();
|
||
|
|
const resultEl = document.getElementById('edit-result');
|
||
|
|
const bodyEl = document.getElementById('edit-result-body');
|
||
|
|
bodyEl.textContent = JSON.stringify(data, null, 2);
|
||
|
|
resultEl.classList.remove('hidden');
|
||
|
|
if (dryRun) {
|
||
|
|
document.getElementById('apply-btn').disabled = false;
|
||
|
|
} else {
|
||
|
|
document.getElementById('apply-btn').disabled = true;
|
||
|
|
if (res.ok) { setTimeout(() => { document.getElementById('edit-modal').close(); window.location.reload(); }, 800); }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
{% endif %}
|
||
|
|
{% endblock content %}
|