Add REST API for preset-based generation and fallback cover images

REST API (routes/api.py): Three endpoints behind API key auth for
programmatic image generation via presets — list presets, queue
generation with optional overrides, and poll job status.

Shared generation logic extracted from routes/presets.py into
services/generation.py so both web UI and API use the same code path.

Fallback covers: library index pages now show a random generated image
at reduced opacity when no cover is assigned, instead of "No Image".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-03-15 21:19:12 +00:00
parent d756ea1d0e
commit 7d79e626a5
20 changed files with 719 additions and 166 deletions

View File

@@ -141,6 +141,25 @@
<button type="submit" class="btn btn-primary btn-lg">Save All Settings</button>
</div>
</form>
<hr>
<h5 class="mb-3 text-primary">REST API Key</h5>
<p class="text-muted small">Generate an API key to use the <code>/api/v1/</code> endpoints for programmatic image generation.</p>
<div class="mb-3">
<div class="input-group">
<input type="password" class="form-control font-monospace" id="api-key-display"
value="{{ settings.api_key or '' }}" readonly
placeholder="No API key generated yet">
<button class="btn btn-outline-secondary" type="button" id="api-key-toggle" title="Show/hide key">
<i class="bi bi-eye"></i> Show
</button>
<button class="btn btn-outline-secondary" type="button" id="api-key-copy" title="Copy to clipboard">
<i class="bi bi-clipboard"></i> Copy
</button>
<button class="btn btn-outline-primary" type="button" id="api-key-regen">Regenerate</button>
</div>
</div>
</div>
</div>
</div>
@@ -232,6 +251,54 @@
});
}
// API Key Management
const apiKeyDisplay = document.getElementById('api-key-display');
const apiKeyToggle = document.getElementById('api-key-toggle');
const apiKeyCopy = document.getElementById('api-key-copy');
const apiKeyRegen = document.getElementById('api-key-regen');
if (apiKeyToggle) {
apiKeyToggle.addEventListener('click', () => {
if (apiKeyDisplay.type === 'password') {
apiKeyDisplay.type = 'text';
apiKeyToggle.innerHTML = '<i class="bi bi-eye-slash"></i> Hide';
} else {
apiKeyDisplay.type = 'password';
apiKeyToggle.innerHTML = '<i class="bi bi-eye"></i> Show';
}
});
}
if (apiKeyCopy) {
apiKeyCopy.addEventListener('click', () => {
if (apiKeyDisplay.value) {
navigator.clipboard.writeText(apiKeyDisplay.value);
apiKeyCopy.innerHTML = '<i class="bi bi-check"></i> Copied';
setTimeout(() => { apiKeyCopy.innerHTML = '<i class="bi bi-clipboard"></i> Copy'; }, 1500);
}
});
}
if (apiKeyRegen) {
apiKeyRegen.addEventListener('click', async () => {
if (!confirm('Generate a new API key? Any existing key will stop working.')) return;
apiKeyRegen.disabled = true;
try {
const resp = await fetch('/api/key/regenerate', { method: 'POST' });
const data = await resp.json();
if (data.api_key) {
apiKeyDisplay.value = data.api_key;
apiKeyDisplay.type = 'text';
apiKeyToggle.innerHTML = '<i class="bi bi-eye-slash"></i> Hide';
}
} catch (err) {
alert('Failed to regenerate API key.');
} finally {
apiKeyRegen.disabled = false;
}
});
}
// Local Model Loading
const connectLocalBtn = document.getElementById('connect-local-btn');
const localModelSelect = document.getElementById('local_model');