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:
+68
-18
@@ -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;
|
||||
|
||||
+67
-12
@@ -2,9 +2,10 @@
|
||||
{% block title %}Beállítások — Recept Importáló{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2>Mealie kapcsolat</h2>
|
||||
<form method="POST" action="/settings">
|
||||
<form method="POST" action="/settings">
|
||||
<div class="card">
|
||||
<h2>Mealie kapcsolat</h2>
|
||||
|
||||
<label for="mealie_url">Mealie URL</label>
|
||||
<input type="url" id="mealie_url" name="mealie_url"
|
||||
value="{{ cfg.mealie_url }}"
|
||||
@@ -20,26 +21,56 @@
|
||||
</p>
|
||||
|
||||
<div class="flex">
|
||||
<button type="submit" class="btn btn-primary">Mentés</button>
|
||||
<button type="button" class="btn btn-secondary" id="testBtn" onclick="testConnection()">
|
||||
<button type="button" class="btn btn-secondary" id="testMealieBtn" onclick="testMealie()">
|
||||
Kapcsolat tesztelése
|
||||
</button>
|
||||
<span id="testResult"></span>
|
||||
<span id="testMealieResult"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Tandoor kapcsolat</h2>
|
||||
|
||||
<label for="tandoor_url">Tandoor URL</label>
|
||||
<input type="url" id="tandoor_url" name="tandoor_url"
|
||||
value="{{ cfg.tandoor_url }}"
|
||||
placeholder="https://recipes.example.com">
|
||||
|
||||
<label for="tandoor_api_key">API kulcs</label>
|
||||
<input type="password" id="tandoor_api_key" name="tandoor_api_key"
|
||||
value="{{ cfg.tandoor_api_key }}"
|
||||
placeholder="Tandoor API token">
|
||||
<p class="text-dim mb-2" style="font-size:0.85rem;">
|
||||
Az API kulcsot a Tandoor-ban itt hozhatod létre:
|
||||
<em>Settings → API Browser → Auth Token</em>
|
||||
</p>
|
||||
|
||||
<div class="flex">
|
||||
<button type="button" class="btn btn-secondary" id="testTandoorBtn" onclick="testTandoor()">
|
||||
Kapcsolat tesztelése
|
||||
</button>
|
||||
<span id="testTandoorResult"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<button type="submit" class="btn btn-primary">Mentés</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function testConnection() {
|
||||
const btn = document.getElementById('testBtn');
|
||||
const result = document.getElementById('testResult');
|
||||
async function testMealie() {
|
||||
const btn = document.getElementById('testMealieBtn');
|
||||
const result = document.getElementById('testMealieResult');
|
||||
btn.disabled = true;
|
||||
result.innerHTML = '<span class="spinner"></span>';
|
||||
|
||||
try {
|
||||
const form = new FormData(document.querySelector('form'));
|
||||
const form = new FormData();
|
||||
form.append('mealie_url', document.getElementById('mealie_url').value);
|
||||
form.append('mealie_api_key', document.getElementById('mealie_api_key').value);
|
||||
const resp = await fetch('/settings/test', { method: 'POST', body: form });
|
||||
const data = await resp.json();
|
||||
if (data.ok) {
|
||||
@@ -53,5 +84,29 @@ async function testConnection() {
|
||||
}
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
async function testTandoor() {
|
||||
const btn = document.getElementById('testTandoorBtn');
|
||||
const result = document.getElementById('testTandoorResult');
|
||||
btn.disabled = true;
|
||||
result.innerHTML = '<span class="spinner"></span>';
|
||||
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append('tandoor_url', document.getElementById('tandoor_url').value);
|
||||
form.append('tandoor_api_key', document.getElementById('tandoor_api_key').value);
|
||||
const resp = await fetch('/settings/test-tandoor', { method: 'POST', body: form });
|
||||
const data = await resp.json();
|
||||
if (data.ok) {
|
||||
const v = data.data.version || '?';
|
||||
result.innerHTML = '<span class="text-success">✓ Kapcsolódva (Tandoor v' + v + ')</span>';
|
||||
} else {
|
||||
result.innerHTML = '<span class="text-danger">✗ ' + data.error + '</span>';
|
||||
}
|
||||
} catch (e) {
|
||||
result.innerHTML = '<span class="text-danger">✗ Hálózati hiba</span>';
|
||||
}
|
||||
btn.disabled = false;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user