Files
xAdmin 41e08611c9 Implement optional single-image intake, Signal ingestion, and multimodal vision analysis
- Add image intake service with format validation (JPEG, PNG, WebP) and EXIF/GPS stripping
- Enforce strict single-image rule across Web and Signal attachment channels
- Implement token-optimized vision downscaling and JPEG compression
- Add IMAGE_CONTEXT pipeline stage with OmniRoute vision routing and resilient failover
- Seed and manage versioned idea-image-interpreter prompt in catalog
- Update Web UI with responsive image picker, preview chip, and Visual Context tab
- Add comprehensive automated test suite in test_image_intake.py
- Update README and Labyricorn devlog
2026-08-23 01:40:22 -07:00

447 lines
25 KiB
HTML

{% extends "base.html" %}
{% block title %}Admin Control Center - ThinkStorm{% endblock %}
{% block content %}
<div style="margin-bottom: 2rem;">
<div style="display:flex; align-items:center; gap:0.6rem; margin-bottom:0.4rem;">
<span class="badge badge-active" style="font-size:0.8rem;">Administrator</span>
</div>
<h1 style="font-size: 2.2rem; margin-bottom: 0.5rem;">Orchestrator Command Center</h1>
<p style="color: var(--text-secondary);">Manage prompts, immutable versions, external service adapters, queues, moderation, and token metrics.</p>
</div>
<div class="tab-container glass-card" style="padding:1.75rem;">
<div class="tab-nav">
<button class="tab-btn active" data-tab="adm-prompts">📝 Prompts & Versions</button>
<button class="tab-btn" data-tab="adm-profiles">🧩 Prompt Profiles</button>
<button class="tab-btn" data-tab="adm-services">🔌 Service Adapters</button>
<button class="tab-btn" data-tab="adm-jobs">⚡ Job Queue & Provenance</button>
<button class="tab-btn" data-tab="adm-quarantine">🛡️ Quarantine & Moderation</button>
<button class="tab-btn" data-tab="adm-trash">🗑️ Trash Bin ({{ trashed_count }})</button>
<button class="tab-btn" data-tab="adm-metrics">📊 Token Accounting & Audits</button>
</div>
<!-- 1. Prompts Management -->
<div id="adm-prompts" class="tab-panel active">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.25rem;">
<div>
<h3 style="font-size:1.15rem;">Prompt Catalog & Immutable Versioning</h3>
<p style="color:var(--text-secondary); font-size:0.88rem;">Editing a prompt creates a new version record. Historical runs remain immutably tied to their original version.</p>
</div>
</div>
<div style="display:flex; flex-direction:column; gap:1rem;">
{% for p in prompts %}
<div style="background:rgba(0,0,0,0.3); border:1px solid var(--border-glass); border-radius:var(--radius-md); padding:1.25rem;">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.75rem; flex-wrap:wrap; gap:0.5rem;">
<div>
<div style="display:flex; align-items:center; gap:0.5rem;">
<span style="font-family:var(--font-mono); font-weight:700; color:var(--accent-primary);">{{ p.id }}</span>
<span class="badge" style="background:rgba(99,102,241,0.15); color:#a5b4fc;">v{{ p.current_version }}</span>
<span class="badge" style="background:rgba(255,255,255,0.06);">{{ p.stage }}</span>
<span class="badge" style="background:rgba(16,185,129,0.1); color:#34d399;">Policy: {{ p.model_policy }}</span>
</div>
<h4 style="font-size:1.1rem; margin-top:0.3rem;">{{ p.name }}</h4>
<p style="color:var(--text-secondary); font-size:0.85rem;">{{ p.description }}</p>
</div>
<div style="display:flex; gap:0.5rem;">
<button onclick="openPromptEditor('{{ p.id }}')" class="btn btn-secondary btn-sm">✏️ Edit (New Version)</button>
<button onclick="togglePrompt('{{ p.id }}')" class="btn btn-secondary btn-sm">{% if p.enabled %}Disable{% else %}Enable{% endif %}</button>
</div>
</div>
<details style="margin-top:0.75rem;">
<summary style="cursor:pointer; font-size:0.82rem; color:var(--text-muted); font-family:var(--font-mono);">
View Current Prompts & Hash ({{ p.prompt_hash[:20] }}...)
</summary>
<div style="margin-top:0.75rem; display:grid; grid-template-columns:1fr 1fr; gap:1rem; font-size:0.85rem;">
<div>
<strong style="color:var(--text-secondary);">System Prompt:</strong>
<pre style="margin-top:0.3rem; white-space:pre-wrap; max-height:220px; overflow-y:auto;">{{ p.system_prompt }}</pre>
</div>
<div>
<strong style="color:var(--text-secondary);">User Prompt Template:</strong>
<pre style="margin-top:0.3rem; white-space:pre-wrap; max-height:220px; overflow-y:auto;">{{ p.user_prompt_template }}</pre>
</div>
</div>
</details>
</div>
{% endfor %}
</div>
</div>
<!-- 2. Profiles -->
<div id="adm-profiles" class="tab-panel">
<h3 style="font-size:1.15rem; margin-bottom:0.5rem;">Prompt Profiles</h3>
<p style="color:var(--text-secondary); font-size:0.88rem; margin-bottom:1.25rem;">Prompt profiles group stage-specific prompts for different idea archetypes.</p>
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); gap:1.25rem;">
{% for prof in profiles %}
<div style="background:rgba(0,0,0,0.3); border:1px solid var(--border-glass); border-radius:var(--radius-md); padding:1.25rem;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem;">
<h4 style="font-size:1.1rem; color:var(--accent-primary);">{{ prof.name }}</h4>
{% if prof.is_default %}<span class="badge badge-success">Default</span>{% endif %}
</div>
<p style="color:var(--text-secondary); font-size:0.85rem; margin-bottom:1rem;">{{ prof.description }}</p>
<div style="font-family:var(--font-mono); font-size:0.8rem; background:rgba(0,0,0,0.4); padding:0.75rem; border-radius:var(--radius-sm);">
{% for stage, p_str in prof.prompt_assignments.items() %}
<div style="display:flex; justify-content:space-between; margin-bottom:0.25rem;">
<span style="color:var(--text-muted);">{{ stage }}:</span>
<span style="color:var(--text-primary);">{{ p_str }}</span>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- 3. Services -->
<div id="adm-services" class="tab-panel">
<h3 style="font-size:1.15rem; margin-bottom:0.5rem;">External Service Adapters</h3>
<p style="color:var(--text-secondary); font-size:0.88rem; margin-bottom:1.25rem;">Live health diagnostics and endpoints configuration for orchestrated services.</p>
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(320px, 1fr)); gap:1.25rem;">
{% for s in services %}
<div style="background:rgba(0,0,0,0.3); border:1px solid var(--border-glass); border-radius:var(--radius-md); padding:1.25rem;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem;">
<h4 style="font-size:1.1rem;">{{ s.name }}</h4>
<span class="badge {% if s.health_status == 'HEALTHY' %}badge-available{% else %}badge-claimed{% endif %}">
{{ s.health_status }}
</span>
</div>
<div style="font-size:0.85rem; color:var(--text-secondary); margin-bottom:0.5rem; word-break:break-all;">
<strong>Endpoint:</strong> <span style="font-family:var(--font-mono);">{{ s.endpoint }}</span>
</div>
{% if s.api_key_masked %}
<div style="font-size:0.85rem; color:var(--text-secondary); margin-bottom:0.75rem;">
<strong>API Key:</strong> <span style="font-family:var(--font-mono);">{{ s.api_key_masked }}</span>
</div>
{% endif %}
{% if s.last_tested_at %}
<div style="font-size:0.75rem; color:var(--text-muted); margin-bottom:1rem;">
Last Checked: {{ s.last_tested_at[:19] }}Z
</div>
{% endif %}
<button id="test-btn-{{ s.id }}" onclick="testService('{{ s.id }}')" class="btn btn-secondary btn-sm" style="width:100%;">
⚡ Test Connection
</button>
</div>
{% endfor %}
</div>
</div>
<!-- 4. Job Observability -->
<div id="adm-jobs" class="tab-panel">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.25rem;">
<h3 style="font-size:1.15rem;">Job Queue & Provenance Observability</h3>
<div style="display:flex; gap:1rem; font-family:var(--font-mono); font-size:0.85rem;">
<span>Foreground: <strong>{{ jobs_data.queue.foreground_queued }}</strong></span>
<span>Background: <strong>{{ jobs_data.queue.background_queued }}</strong></span>
<span>Active: <strong>{{ jobs_data.queue.active_jobs_count }}</strong></span>
</div>
</div>
<div style="overflow-x:auto;">
<table style="width:100%; border-collapse:collapse; font-size:0.85rem; text-align:left;">
<thead>
<tr style="border-bottom:1px solid var(--border-glass); color:var(--text-muted);">
<th style="padding:0.6rem;">Run ID</th>
<th style="padding:0.6rem;">Idea ID</th>
<th style="padding:0.6rem;">Processor</th>
<th style="padding:0.6rem;">Model</th>
<th style="padding:0.6rem;">Tokens</th>
<th style="padding:0.6rem;">Duration</th>
<th style="padding:0.6rem;">Status</th>
<th style="padding:0.6rem;">Action</th>
</tr>
</thead>
<tbody>
{% for r in jobs_data.recent_runs %}
<tr style="border-bottom:1px solid rgba(255,255,255,0.04);">
<td style="padding:0.6rem; font-family:var(--font-mono); color:var(--accent-primary);">{{ r.id }}</td>
<td style="padding:0.6rem; font-family:var(--font-mono);"><a href="/ideas/{{ r.idea_id }}">{{ r.idea_id }}</a></td>
<td style="padding:0.6rem;"><strong>{{ r.processor_name }}</strong></td>
<td style="padding:0.6rem; font-family:var(--font-mono); font-size:0.78rem;">{{ r.resolved_model }}</td>
<td style="padding:0.6rem; font-family:var(--font-mono);">{{ r.total_tokens }}</td>
<td style="padding:0.6rem; font-family:var(--font-mono);">{{ r.duration_ms }}ms</td>
<td style="padding:0.6rem;"><span class="badge badge-success">{{ r.status }}</span></td>
<td style="padding:0.6rem;">
<button onclick="retryJob('{{ r.idea_id }}')" class="btn btn-secondary btn-sm" style="padding:0.2rem 0.5rem; font-size:0.75rem;">Retry Idea</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- 5. Quarantine & Moderation -->
<div id="adm-quarantine" class="tab-panel">
<h3 style="font-size:1.15rem; margin-bottom:0.5rem;">Quarantined Submissions & Moderation</h3>
<p style="color:var(--text-secondary); font-size:0.88rem; margin-bottom:1.25rem;">Submissions flagged with malicious or suspicious URLs are held for administrative review before retrieval.</p>
{% if quarantined and quarantined|length > 0 %}
<div style="display:flex; flex-direction:column; gap:1.25rem;">
{% for q in quarantined %}
<div style="background:rgba(239,68,68,0.05); border:1px solid rgba(239,68,68,0.25); border-radius:var(--radius-md); padding:1.25rem;">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.75rem;">
<div>
<span style="font-family:var(--font-mono); font-weight:700; color:var(--color-danger);">{{ q.id }}</span>
<span class="badge badge-quarantined" style="margin-left:0.5rem;">QUARANTINED</span>
<div style="font-size:0.85rem; color:var(--text-muted); margin-top:0.25rem;">Submitted: {{ q.submitted_at[:19] }}Z</div>
</div>
<div style="display:flex; gap:0.5rem;">
<button onclick="quarantineDecision('{{ q.id }}', 'APPROVE')" class="btn btn-success btn-sm">Approve & Release</button>
<button onclick="quarantineDecision('{{ q.id }}', 'REJECT')" class="btn btn-danger btn-sm">Reject Submission</button>
</div>
</div>
<div class="markdown-view-container" data-markdown-view style="margin-bottom:0.75rem;">
<div class="markdown-view-header" style="margin-bottom:0.4rem;">
<div class="markdown-view-actions">
<div class="markdown-toggle-pill">
<button type="button" class="btn-toggle-view active" data-view="formatted" title="Formatted View">
<span>👁️ Formatted</span>
</button>
<button type="button" class="btn-toggle-view" data-view="code" title="Code / Raw View">
<span>💻 Code</span>
</button>
</div>
<button type="button" class="btn-copy-markdown" onclick="copyMarkdownFromContainer(this)" title="Copy text">
<span>📋 Copy</span>
</button>
</div>
</div>
<div class="markdown-view-body" style="background:rgba(0,0,0,0.3); padding:0.75rem; border-radius:var(--radius-sm); border:1px solid var(--border-glass); font-size:0.9rem;">
<div class="markdown-formatted markdown-body"></div>
<div class="markdown-code" style="display:none; background:transparent; border:none; padding:0; margin:0;">
<pre style="margin:0; background:transparent; border:none; padding:0; line-height:1.6;"><code>{{ q.original_text }}</code></pre>
</div>
<textarea class="raw-markdown-source" style="display:none;">{{ q.original_text }}</textarea>
</div>
</div>
<div>
<h5 style="font-size:0.85rem; color:var(--text-secondary); margin-bottom:0.4rem;">Flagged URLs:</h5>
{% for u in q.urls %}
<div style="font-family:var(--font-mono); font-size:0.8rem; background:rgba(0,0,0,0.4); padding:0.4rem 0.6rem; border-radius:var(--radius-sm); margin-bottom:0.3rem; display:flex; justify-content:space-between;">
<span>{{ u.url }}</span>
<span class="badge badge-malicious">{{ u.safety_state }}</span>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<p style="color:var(--text-muted);">No quarantined ideas requiring review.</p>
{% endif %}
</div>
<!-- 6. Trash Bin Management -->
<div id="adm-trash" class="tab-panel">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.25rem; flex-wrap:wrap; gap:1rem;">
<div>
<h3 style="font-size:1.15rem; color:#f87171;">Trash Bin & Idea Retention</h3>
<p style="color:var(--text-secondary); font-size:0.88rem;">Items moved to trash are withheld from incubation until restored or permanently emptied.</p>
</div>
{% if trashed_ideas and trashed_ideas|length > 0 %}
<button onclick="emptyTrash()" class="btn btn-danger" style="display:flex; align-items:center; gap:0.4rem;">
<span>🗑️ Empty Trash ({{ trashed_ideas|length }})</span>
</button>
{% endif %}
</div>
{% if trashed_ideas and trashed_ideas|length > 0 %}
<div style="display:flex; flex-direction:column; gap:1rem;">
{% for t in trashed_ideas %}
<div style="background:rgba(0,0,0,0.3); border:1px solid rgba(239,68,68,0.25); border-radius:var(--radius-md); padding:1.25rem;">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.75rem; flex-wrap:wrap; gap:0.5rem;">
<div>
<div style="display:flex; align-items:center; gap:0.5rem;">
<a href="/ideas/{{ t.id }}" style="font-family:var(--font-mono); font-weight:700; color:var(--accent-primary);">{{ t.id }}</a>
<span class="badge badge-trashed">TRASHED</span>
{% if t.previous_lifecycle_state %}
<span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.7rem;">Prev: {{ t.previous_lifecycle_state }}</span>
{% endif %}
</div>
<h4 style="font-size:1.05rem; margin-top:0.3rem;">{{ t.title or "Untitled Idea" }}</h4>
<div style="font-size:0.8rem; color:var(--text-muted); margin-top:0.2rem; font-family:var(--font-mono);">
Submitted: {{ t.submitted_at[:19] }}Z {% if t.trashed_at %}• Trashed: {{ t.trashed_at[:19] }}Z{% endif %}
</div>
</div>
<div style="display:flex; gap:0.5rem;">
<button onclick="restoreIdea('{{ t.id }}')" class="btn btn-secondary btn-sm" style="color:#34d399;">♻️ Restore</button>
<button onclick="deleteIdeaPermanently('{{ t.id }}')" class="btn btn-danger btn-sm">❌ Delete Forever</button>
</div>
</div>
<div class="markdown-view-container" data-markdown-view>
<div class="markdown-view-header" style="margin-bottom:0.4rem;">
<div class="markdown-view-actions">
<div class="markdown-toggle-pill">
<button type="button" class="btn-toggle-view active" data-view="formatted" title="Formatted View">
<span>👁️ Formatted</span>
</button>
<button type="button" class="btn-toggle-view" data-view="code" title="Code / Raw View">
<span>💻 Code</span>
</button>
</div>
<button type="button" class="btn-copy-markdown" onclick="copyMarkdownFromContainer(this)" title="Copy text">
<span>📋 Copy</span>
</button>
</div>
</div>
<div class="markdown-view-body" style="background:rgba(0,0,0,0.3); padding:0.75rem; border-radius:var(--radius-sm); border:1px solid var(--border-glass); font-size:0.88rem; color:var(--text-secondary);">
<div class="markdown-formatted markdown-body"></div>
<div class="markdown-code" style="display:none; background:transparent; border:none; padding:0; margin:0;">
<pre style="margin:0; background:transparent; border:none; padding:0; line-height:1.6;"><code>{{ t.original_text }}</code></pre>
</div>
<textarea class="raw-markdown-source" style="display:none;">{{ t.original_text }}</textarea>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div style="text-align:center; padding:2.5rem; background:rgba(0,0,0,0.2); border-radius:var(--radius-md); border:1px solid var(--border-glass);">
<p style="color:var(--text-muted); margin:0;">Trash is empty. No ideas are currently in the trash bin.</p>
</div>
{% endif %}
</div>
<!-- 7. Metrics & Audits -->
<div id="adm-metrics" class="tab-panel">
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:1.25rem; margin-bottom:2rem;">
<div style="background:rgba(0,0,0,0.3); padding:1.25rem; border-radius:var(--radius-md); border:1px solid var(--border-glass);">
<div style="font-size:0.85rem; color:var(--text-secondary);">Total Tokens</div>
<div style="font-size:1.8rem; font-weight:700; font-family:var(--font-mono); color:var(--accent-primary);">{{ metrics.overall.total_tokens }}</div>
</div>
<div style="background:rgba(0,0,0,0.3); padding:1.25rem; border-radius:var(--radius-md); border:1px solid var(--border-glass);">
<div style="font-size:0.85rem; color:var(--text-secondary);">Input Tokens</div>
<div style="font-size:1.8rem; font-weight:700; font-family:var(--font-mono);">{{ metrics.overall.input_tokens }}</div>
</div>
<div style="background:rgba(0,0,0,0.3); padding:1.25rem; border-radius:var(--radius-md); border:1px solid var(--border-glass);">
<div style="font-size:0.85rem; color:var(--text-secondary);">Output Tokens</div>
<div style="font-size:1.8rem; font-weight:700; font-family:var(--font-mono);">{{ metrics.overall.output_tokens }}</div>
</div>
<div style="background:rgba(0,0,0,0.3); padding:1.25rem; border-radius:var(--radius-md); border:1px solid var(--border-glass);">
<div style="font-size:0.85rem; color:var(--text-secondary);">Processor Runs</div>
<div style="font-size:1.8rem; font-weight:700; font-family:var(--font-mono);">{{ metrics.overall.runs_count }}</div>
</div>
</div>
<h4 style="font-size:1.1rem; margin-bottom:0.75rem;">Recent Administrative Audit Log</h4>
<div style="overflow-x:auto;">
<table style="width:100%; border-collapse:collapse; font-size:0.82rem; text-align:left;">
<thead>
<tr style="border-bottom:1px solid var(--border-glass); color:var(--text-muted);">
<th style="padding:0.5rem;">Timestamp</th>
<th style="padding:0.5rem;">User</th>
<th style="padding:0.5rem;">Action</th>
<th style="padding:0.5rem;">Entity</th>
<th style="padding:0.5rem;">Details</th>
</tr>
</thead>
<tbody>
{% for a in audit_logs %}
<tr style="border-bottom:1px solid rgba(255,255,255,0.04);">
<td style="padding:0.5rem; font-family:var(--font-mono); color:var(--text-muted);">{{ a.created_at[:19] }}Z</td>
<td style="padding:0.5rem;"><strong>{{ a.user_id }}</strong></td>
<td style="padding:0.5rem;"><span class="badge badge-claimed">{{ a.action }}</span></td>
<td style="padding:0.5rem; font-family:var(--font-mono);">{{ a.entity_type }}:{{ a.entity_id }}</td>
<td style="padding:0.5rem; font-family:var(--font-mono); font-size:0.75rem;">{{ a.details }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal for Prompt Editor -->
<div id="prompt-modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.8); backdrop-filter:blur(8px); z-index:200; align-items:center; justify-content:center; padding:1.5rem;">
<div class="glass-card" style="width:100%; max-width:700px; padding:2rem; max-height:90vh; overflow-y:auto;">
<h3 id="modal-prompt-title" style="font-size:1.3rem; margin-bottom:0.5rem;">Edit Prompt</h3>
<p style="color:var(--text-muted); font-size:0.85rem; margin-bottom:1.25rem;">Saving creates a new immutable version without breaking historical execution provenance.</p>
<form id="prompt-edit-form">
<input type="hidden" id="edit-prompt-id">
<div class="form-group">
<label class="form-label">System Prompt</label>
<textarea id="edit-system-prompt" class="form-textarea" rows="6" required></textarea>
</div>
<div class="form-group">
<label class="form-label">User Prompt Template (use {{submission_text}} or {{title}})</label>
<textarea id="edit-user-prompt" class="form-textarea" rows="6" required></textarea>
</div>
<div style="display:flex; justify-content:flex-end; gap:0.75rem; margin-top:1.5rem;">
<button type="button" onclick="closePromptEditor()" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn btn-primary">Save New Version</button>
</div>
</form>
</div>
</div>
<script>
async function openPromptEditor(promptId) {
const res = await fetch(`/api/admin/prompts/${promptId}`);
const data = await res.json();
document.getElementById('edit-prompt-id').value = data.id;
document.getElementById('modal-prompt-title').innerText = `Edit: ${data.name} (Current: v${data.version})`;
document.getElementById('edit-system-prompt').value = data.system_prompt;
document.getElementById('edit-user-prompt').value = data.user_prompt_template;
document.getElementById('prompt-modal').style.display = 'flex';
}
function closePromptEditor() {
document.getElementById('prompt-modal').style.display = 'none';
}
document.getElementById('prompt-edit-form').addEventListener('submit', async (e) => {
e.preventDefault();
const promptId = document.getElementById('edit-prompt-id').value;
const systemPrompt = document.getElementById('edit-system-prompt').value;
const userPrompt = document.getElementById('edit-user-prompt').value;
try {
const res = await fetch(`/api/admin/prompts/${promptId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
system_prompt: systemPrompt,
user_prompt_template: userPrompt
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Failed to update prompt');
showToast(data.message, 'success');
closePromptEditor();
setTimeout(() => window.location.reload(), 800);
} catch (err) {
showToast(err.message, 'danger');
}
});
async function togglePrompt(promptId) {
try {
const res = await fetch(`/api/admin/prompts/${promptId}/toggle`, { method: 'POST' });
const data = await res.json();
showToast(data.message, 'info');
setTimeout(() => window.location.reload(), 600);
} catch (err) {
showToast(err.message, 'danger');
}
}
</script>
{% endblock %}