feat: Tandoor integration — settings, test connection, import, duplicate detection

Add TandoorClient (app/tandoor.py) with full recipe creation, image upload,
and duplicate detection via the Tandoor REST API. Settings page now has
separate Mealie and Tandoor sections. Import page shows both send buttons
based on which services are configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:29:58 +01:00
parent f7810ba33d
commit 458b1e362a
6 changed files with 476 additions and 68 deletions
+68 -18
View File
@@ -180,9 +180,16 @@
<button class="add-btn mt-1 mb-2" onclick="addInstruction('')">+ Lépés hozzáadása</button>
<div class="flex mt-2">
<button class="btn btn-success" id="sendBtn" onclick="sendToMealie()">
{% if has_mealie %}
<button class="btn btn-success" id="sendMealieBtn" onclick="sendToMealie()">
Importálás Mealie-be
</button>
{% endif %}
{% if has_tandoor %}
<button class="btn btn-success" id="sendTandoorBtn" onclick="sendToTandoor()">
Importálás Tandoor-ba
</button>
{% endif %}
<span id="sendStatus"></span>
</div>
</div>
@@ -192,11 +199,7 @@
<div class="result-card" id="resultCard">
<div class="card">
<h2 class="text-success">Recept sikeresen importálva!</h2>
<p class="mt-1">
<a id="resultLink" href="#" target="_blank" style="color:var(--accent);">
Megnyitás Mealie-ben →
</a>
</p>
<p class="mt-1" id="resultLinks"></p>
</div>
</div>
{% endblock %}
@@ -216,6 +219,7 @@ async function scrapeRecipe() {
document.getElementById('previewCard').classList.remove('visible');
document.getElementById('resultCard').classList.remove('visible');
Object.keys(importedLinks).forEach(k => delete importedLinks[k]);
try {
const form = new FormData();
@@ -233,10 +237,17 @@ async function scrapeRecipe() {
populatePreview(currentRecipe);
document.getElementById('previewCard').classList.add('visible');
const warnings = [];
if (data.duplicate) {
status.innerHTML = '<span class="text-warning">⚠ Ez a recept már létezik Mealie-ben: '
+ '<a href="' + escHtml(data.duplicate.url) + '" target="_blank" style="color:var(--accent)">'
+ escHtml(data.duplicate.name) + '</a></span>';
warnings.push('Mealie: <a href="' + escHtml(data.duplicate.url) + '" target="_blank" style="color:var(--accent)">'
+ escHtml(data.duplicate.name) + '</a>');
}
if (data.tandoor_duplicate) {
warnings.push('Tandoor: <a href="' + escHtml(data.tandoor_duplicate.url) + '" target="_blank" style="color:var(--accent)">'
+ escHtml(data.tandoor_duplicate.name) + '</a>');
}
if (warnings.length > 0) {
status.innerHTML = '<span class="text-warning">⚠ Ez a recept már létezik: ' + warnings.join(' | ') + '</span>';
} else {
status.innerHTML = '<span class="text-success">✓ Beolvasva</span>';
}
@@ -351,17 +362,16 @@ function gatherRecipe() {
};
}
const importedLinks = {};
async function sendToMealie() {
const recipe = gatherRecipe();
if (!recipe.title) {
alert('A recept neve kötelező!');
return;
}
if (!recipe.title) { alert('A recept neve kötelező!'); return; }
const btn = document.getElementById('sendBtn');
const btn = document.getElementById('sendMealieBtn');
const status = document.getElementById('sendStatus');
btn.disabled = true;
status.innerHTML = '<span class="spinner"></span> Importálás...';
status.innerHTML = '<span class="spinner"></span> Importálás Mealie-be...';
try {
const resp = await fetch('/send', {
@@ -377,15 +387,55 @@ async function sendToMealie() {
return;
}
status.innerHTML = '';
document.getElementById('resultLink').href = data.url;
document.getElementById('resultCard').classList.add('visible');
status.innerHTML = '<span class="text-success">✓ Mealie kész</span>';
importedLinks['Mealie'] = data.url;
showResultCard();
} catch (e) {
status.innerHTML = '<span class="text-danger">Hálózati hiba: ' + e.message + '</span>';
}
btn.disabled = false;
}
async function sendToTandoor() {
const recipe = gatherRecipe();
if (!recipe.title) { alert('A recept neve kötelező!'); return; }
const btn = document.getElementById('sendTandoorBtn');
const status = document.getElementById('sendStatus');
btn.disabled = true;
status.innerHTML = '<span class="spinner"></span> Importálás Tandoor-ba...';
try {
const resp = await fetch('/send-tandoor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(recipe),
});
const data = await resp.json();
if (!data.ok) {
status.innerHTML = '<span class="text-danger">Hiba: ' + data.error + '</span>';
btn.disabled = false;
return;
}
status.innerHTML = '<span class="text-success">✓ Tandoor kész</span>';
importedLinks['Tandoor'] = data.url;
showResultCard();
} catch (e) {
status.innerHTML = '<span class="text-danger">Hálózati hiba: ' + e.message + '</span>';
}
btn.disabled = false;
}
function showResultCard() {
const links = Object.entries(importedLinks).map(([name, url]) =>
'<a href="' + escHtml(url) + '" target="_blank" style="color:var(--accent);">Megnyitás ' + name + '-ben →</a>'
).join('<br>');
document.getElementById('resultLinks').innerHTML = links;
document.getElementById('resultCard').classList.add('visible');
}
function escHtml(s) {
const d = document.createElement('div');
d.textContent = s;