Add danbooru-mcp auto-start, git sync, status API endpoints, navbar status indicators, and LLM format retry

- app.py: add subprocess import; add _ensure_mcp_repo() to clone/pull
  danbooru-mcp from https://git.liveaodh.com/aodhan/danbooru-mcp into
  tools/danbooru-mcp/ at startup; add ensure_mcp_server_running() which
  calls _ensure_mcp_repo() then starts the Docker container if not running;
  add GET /api/status/comfyui and GET /api/status/mcp health endpoints;
  fix call_llm() to retry up to 3 times on unexpected response format
  (KeyError/IndexError), logging the raw response and prompting the LLM
  to respond with valid JSON before each retry
- templates/layout.html: add ComfyUI and MCP status dot indicators to
  navbar; add polling JS that checks both endpoints on load and every 30s
- static/style.css: add .service-status, .status-dot, .status-ok,
  .status-error, .status-checking styles and status-pulse keyframe animation
- .gitignore: add tools/ to exclude the cloned danbooru-mcp repo
This commit is contained in:
Aodhan Collins
2026-03-03 00:57:27 +00:00
parent 0b8802deb5
commit ae7ba961c1
1194 changed files with 17475 additions and 3268 deletions

View File

@@ -1,49 +1,6 @@
{% extends "layout.html" %}
{% block content %}
<style>
.gallery-card { position: relative; overflow: hidden; border-radius: 8px; background: #1a1a1a; cursor: pointer; }
.gallery-card img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; transition: transform 0.2s; }
.gallery-card:hover img { transform: scale(1.04); }
.gallery-card .overlay {
position: absolute; bottom: 0; left: 0; right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.82));
padding: 28px 8px 8px; opacity: 0; transition: opacity 0.2s;
}
.gallery-card:hover .overlay { opacity: 1; }
.gallery-card .cat-badge {
position: absolute; top: 6px; left: 6px;
font-size: 0.65rem; text-transform: uppercase; letter-spacing: .04em;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 8px;
}
@media (min-width: 768px) { .gallery-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
@media (min-width: 1200px) { .gallery-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } }
/* Lightbox */
#lightbox { display:none; position:fixed; inset:0; background:rgba(0,0,0,.92); z-index:9999;
align-items:center; justify-content:center; flex-direction:column; }
#lightbox.active { display:flex; }
#lightbox-img-wrap { position:relative; }
#lightbox-img { max-width:90vw; max-height:80vh; object-fit:contain; border-radius:6px;
box-shadow:0 0 40px rgba(0,0,0,.8); cursor:zoom-in; display:block; }
#lightbox-meta { color:#eee; margin-top:10px; text-align:center; font-size:.85rem; }
#lightbox-hint { color:rgba(255,255,255,.45); font-size:.75rem; margin-top:3px; }
#lightbox-close { position:fixed; top:16px; right:20px; font-size:2rem; color:#fff; cursor:pointer; z-index:10000; line-height:1; }
#lightbox-prompt-btn { position:fixed; bottom:20px; right:20px; z-index:10000; }
/* Prompt modal meta grid */
.meta-grid { display:grid; grid-template-columns:auto 1fr; gap:4px 12px; font-size:.85rem; }
.meta-grid .meta-label { color:#6c757d; white-space:nowrap; font-weight:600; }
.meta-grid .meta-value { font-family:monospace; word-break:break-all; }
.lora-chip { display:inline-flex; align-items:center; gap:4px; background:#f0f0f0;
border-radius:4px; padding:2px 8px; font-size:.8rem; font-family:monospace; margin:2px; }
.lora-chip .lora-strength { color:#6c757d; }
</style>
<div class="d-flex align-items-center justify-content-between mb-3">
<h4 class="mb-0">Gallery
<span class="text-muted fs-6 fw-normal ms-2">{{ total }} image{{ 's' if total != 1 else '' }}</span>
@@ -130,12 +87,13 @@
{% if images %}
<div class="gallery-grid mb-4">
{% set cat_colors = {
'characters': 'primary',
'actions': 'danger',
'outfits': 'success',
'scenes': 'info',
'styles': 'warning',
'detailers': 'secondary',
'characters': 'primary',
'actions': 'danger',
'outfits': 'success',
'scenes': 'info',
'styles': 'warning',
'detailers': 'secondary',
'checkpoints': 'dark',
} %}
{% for img in images %}
<div class="gallery-card"
@@ -162,11 +120,20 @@
<a href="{{ url_for('detail', slug=img.slug) }}"
class="btn btn-sm btn-outline-light py-0 px-2"
onclick="event.stopPropagation()">Open</a>
{% elif img.category == 'checkpoints' %}
<a href="{{ url_for('checkpoint_detail', slug=img.slug) }}"
class="btn btn-sm btn-outline-light py-0 px-2"
onclick="event.stopPropagation()">Open</a>
{% else %}
<a href="{{ url_for('generator') }}?{{ img.category[:-1] }}={{ img.slug }}"
class="btn btn-sm btn-outline-light py-0 px-2"
onclick="event.stopPropagation()">Generator</a>
{% endif %}
<button class="btn btn-sm btn-outline-danger py-0 px-2"
title="Delete"
onclick="event.stopPropagation(); openDeleteModal({{ img.path | tojson }}, {{ img.item_name | tojson }})">
🗑
</button>
</div>
</div>
</div>
@@ -217,9 +184,10 @@
<div id="lightbox-meta"></div>
<div id="lightbox-hint">Click image to open full size · Esc to close</div>
</div>
<button id="lightbox-prompt-btn" class="btn btn-sm btn-light" onclick="event.stopPropagation(); lightboxShowPrompt()">
View Prompt
</button>
<div id="lightbox-actions" onclick="event.stopPropagation()">
<button class="btn btn-sm btn-light" onclick="lightboxShowPrompt()">View Prompt</button>
<button class="btn btn-sm btn-outline-danger" onclick="openDeleteModal(_lightboxPath, _lightboxName); closeLightbox()">Delete</button>
</div>
</div>
<!-- Prompt modal -->
@@ -277,6 +245,24 @@
</div>
</div>
</div>
<!-- Delete confirmation modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Image</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="mb-0">Delete <strong id="deleteItemName"></strong>? The image file will be removed.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Delete</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
@@ -379,8 +365,12 @@ async function showPrompt(imgPath, name, category, slug) {
// Generator link
const genUrl = category === 'characters'
? `/character/${slug}`
: `/generator?${category.replace(/s$/, '')}=${encodeURIComponent(slug)}`;
document.getElementById('openGeneratorBtn').href = genUrl;
: category === 'checkpoints'
? `/checkpoint/${slug}`
: `/generator?${category.replace(/s$/, '')}=${encodeURIComponent(slug)}`;
const genBtn = document.getElementById('openGeneratorBtn');
genBtn.href = genUrl;
genBtn.textContent = (category === 'characters' || category === 'checkpoints') ? 'Open' : 'Open in Generator';
} catch (e) {
document.getElementById('promptPositive').value = 'Error loading metadata.';
} finally {
@@ -397,5 +387,46 @@ function copyField(id, btn) {
setTimeout(() => btn.textContent = orig, 1500);
});
}
// ---- Delete modal ----
let _deletePath = '';
let deleteModal;
document.addEventListener('DOMContentLoaded', () => {
deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
});
function openDeleteModal(path, name) {
_deletePath = path;
document.getElementById('deleteItemName').textContent = name;
deleteModal.show();
}
async function confirmDelete() {
deleteModal.hide();
try {
const res = await fetch('/gallery/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({path: _deletePath}),
});
const data = await res.json();
if (data.status === 'ok') {
const card = document.querySelector(`.gallery-card[data-path="${CSS.escape(_deletePath)}"]`);
if (card) card.remove();
const countEl = document.querySelector('h4 .text-muted');
if (countEl) {
const m = countEl.textContent.match(/(\d+)/);
if (m) {
const n = parseInt(m[1]) - 1;
countEl.textContent = ` ${n} image${n !== 1 ? 's' : ''}`;
}
}
} else {
alert('Delete failed: ' + (data.error || 'unknown error'));
}
} catch (e) {
alert('Delete failed: ' + e);
}
}
</script>
{% endblock %}