Jesús Pérez da083fb9ec
Some checks failed
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
.coder/m
2026-03-29 00:19:56 +00:00

189 lines
8.5 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 %}
{% 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="badge badge-xs font-mono {% if coh.status == 'Ok' %}badge-success{% elif coh.status == 'Warning' %}badge-warning{% else %}badge-error{% endif %}">{{ 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="badge badge-ghost badge-xs font-mono {% if c.kind == 'RustStruct' %}text-orange-400{% elif c.kind == 'NuScript' %}text-cyan-400{% elif c.kind == 'CiPipeline' %}text-purple-400{% else %}text-yellow-400{% endif %}" 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 %}