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:
+6
-3
@@ -1,4 +1,4 @@
|
||||
"""Configuration management — persists Mealie connection settings to a JSON file."""
|
||||
"""Configuration management — persists Mealie and Tandoor connection settings to a JSON file."""
|
||||
|
||||
import json
|
||||
import os
|
||||
@@ -7,13 +7,16 @@ from pathlib import Path
|
||||
DATA_DIR = Path(os.environ.get("DATA_DIR", "/data"))
|
||||
CONFIG_FILE = DATA_DIR / "config.json"
|
||||
|
||||
# Optional internal URL for Docker-to-Docker API calls (avoids Cloudflare hairpin).
|
||||
# The user-facing mealie_url is still used for browser links.
|
||||
# Optional internal URLs for Docker-to-Docker API calls (avoids Cloudflare hairpin).
|
||||
# The user-facing URLs are still used for browser links.
|
||||
MEALIE_INTERNAL_URL = os.environ.get("MEALIE_INTERNAL_URL", "").strip().rstrip("/")
|
||||
TANDOOR_INTERNAL_URL = os.environ.get("TANDOOR_INTERNAL_URL", "").strip().rstrip("/")
|
||||
|
||||
_DEFAULTS = {
|
||||
"mealie_url": "",
|
||||
"mealie_api_key": "",
|
||||
"tandoor_url": "",
|
||||
"tandoor_api_key": "",
|
||||
}
|
||||
|
||||
|
||||
|
||||
+58
-6
@@ -8,6 +8,7 @@ from flask import Flask, render_template, request, redirect, url_for, flash, jso
|
||||
from app import config
|
||||
from app.scraper import scrape
|
||||
from app.mealie import MealieClient
|
||||
from app.tandoor import TandoorClient
|
||||
|
||||
app = Flask(
|
||||
__name__,
|
||||
@@ -28,7 +29,9 @@ VERSION = os.environ.get("VERSION", "dev")
|
||||
def index():
|
||||
"""Redirect to the import page (or settings if not configured)."""
|
||||
cfg = config.load()
|
||||
if not cfg.get("mealie_url") or not cfg.get("mealie_api_key"):
|
||||
has_mealie = bool(cfg.get("mealie_url") and cfg.get("mealie_api_key"))
|
||||
has_tandoor = bool(cfg.get("tandoor_url") and cfg.get("tandoor_api_key"))
|
||||
if not has_mealie and not has_tandoor:
|
||||
return redirect(url_for("settings"))
|
||||
return redirect(url_for("import_page"))
|
||||
|
||||
@@ -41,6 +44,8 @@ def settings():
|
||||
if request.method == "POST":
|
||||
cfg["mealie_url"] = request.form.get("mealie_url", "").strip().rstrip("/")
|
||||
cfg["mealie_api_key"] = request.form.get("mealie_api_key", "").strip()
|
||||
cfg["tandoor_url"] = request.form.get("tandoor_url", "").strip().rstrip("/")
|
||||
cfg["tandoor_api_key"] = request.form.get("tandoor_api_key", "").strip()
|
||||
config.save(cfg)
|
||||
flash("Beállítások mentve.", "success")
|
||||
return redirect(url_for("settings"))
|
||||
@@ -63,14 +68,32 @@ def settings_test():
|
||||
return jsonify({"ok": False, "error": str(exc)})
|
||||
|
||||
|
||||
@app.route("/settings/test-tandoor", methods=["POST"])
|
||||
def settings_test_tandoor():
|
||||
"""AJAX endpoint — test Tandoor connection using form values."""
|
||||
url = (request.form.get("tandoor_url") or "").strip().rstrip("/")
|
||||
key = (request.form.get("tandoor_api_key") or "").strip()
|
||||
if not url or not key:
|
||||
return jsonify({"ok": False, "error": "Nincs megadva Tandoor URL vagy API kulcs."})
|
||||
try:
|
||||
client = TandoorClient(url, key, api_url=config.TANDOOR_INTERNAL_URL)
|
||||
info = client.test_connection()
|
||||
return jsonify({"ok": True, "data": info})
|
||||
except Exception as exc:
|
||||
return jsonify({"ok": False, "error": str(exc)})
|
||||
|
||||
|
||||
@app.route("/import", methods=["GET"])
|
||||
def import_page():
|
||||
"""Show the import form."""
|
||||
cfg = config.load()
|
||||
if not cfg.get("mealie_url") or not cfg.get("mealie_api_key"):
|
||||
flash("Először állítsd be a Mealie kapcsolatot.", "warning")
|
||||
has_mealie = bool(cfg.get("mealie_url") and cfg.get("mealie_api_key"))
|
||||
has_tandoor = bool(cfg.get("tandoor_url") and cfg.get("tandoor_api_key"))
|
||||
if not has_mealie and not has_tandoor:
|
||||
flash("Először állíts be legalább egy szolgáltatást (Mealie vagy Tandoor).", "warning")
|
||||
return redirect(url_for("settings"))
|
||||
return render_template("import.html", cfg=cfg, version=VERSION)
|
||||
return render_template("import.html", cfg=cfg, version=VERSION,
|
||||
has_mealie=has_mealie, has_tandoor=has_tandoor)
|
||||
|
||||
|
||||
@app.route("/scrape", methods=["POST"])
|
||||
@@ -82,8 +105,9 @@ def scrape_url():
|
||||
try:
|
||||
data = scrape(url)
|
||||
|
||||
# Check for duplicate in Mealie
|
||||
# Check for duplicates in Mealie and Tandoor
|
||||
duplicate = None
|
||||
tandoor_duplicate = None
|
||||
cfg = config.load()
|
||||
if cfg.get("mealie_url") and cfg.get("mealie_api_key"):
|
||||
try:
|
||||
@@ -92,8 +116,16 @@ def scrape_url():
|
||||
duplicate = client.find_duplicate(url, data.get("title", ""))
|
||||
except Exception:
|
||||
pass # non-fatal
|
||||
if cfg.get("tandoor_url") and cfg.get("tandoor_api_key"):
|
||||
try:
|
||||
client = TandoorClient(cfg["tandoor_url"], cfg["tandoor_api_key"],
|
||||
api_url=config.TANDOOR_INTERNAL_URL)
|
||||
tandoor_duplicate = client.find_duplicate(url, data.get("title", ""))
|
||||
except Exception:
|
||||
pass # non-fatal
|
||||
|
||||
return jsonify({"ok": True, "data": data, "duplicate": duplicate})
|
||||
return jsonify({"ok": True, "data": data, "duplicate": duplicate,
|
||||
"tandoor_duplicate": tandoor_duplicate})
|
||||
except Exception as exc:
|
||||
return jsonify({"ok": False, "error": str(exc), "trace": traceback.format_exc()})
|
||||
|
||||
@@ -119,6 +151,26 @@ def send_to_mealie():
|
||||
return jsonify({"ok": False, "error": str(exc), "trace": traceback.format_exc()})
|
||||
|
||||
|
||||
@app.route("/send-tandoor", methods=["POST"])
|
||||
def send_to_tandoor():
|
||||
"""AJAX — send edited recipe data to Tandoor."""
|
||||
cfg = config.load()
|
||||
if not cfg.get("tandoor_url") or not cfg.get("tandoor_api_key"):
|
||||
return jsonify({"ok": False, "error": "Tandoor nincs beállítva."})
|
||||
|
||||
payload = request.get_json(silent=True)
|
||||
if not payload:
|
||||
return jsonify({"ok": False, "error": "Érvénytelen kérés."})
|
||||
|
||||
try:
|
||||
client = TandoorClient(cfg["tandoor_url"], cfg["tandoor_api_key"],
|
||||
api_url=config.TANDOOR_INTERNAL_URL)
|
||||
result = client.create_recipe(payload)
|
||||
return jsonify({"ok": True, "id": result["id"], "url": result["url"]})
|
||||
except Exception as exc:
|
||||
return jsonify({"ok": False, "error": str(exc), "trace": traceback.format_exc()})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Health
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
"""Tandoor API client — creates recipes and uploads images."""
|
||||
|
||||
import io
|
||||
import re
|
||||
import requests
|
||||
|
||||
|
||||
class TandoorClient:
|
||||
"""Thin wrapper around the Tandoor REST API."""
|
||||
|
||||
def __init__(self, base_url: str, api_key: str, api_url: str = ""):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_url = api_url.rstrip("/") if api_url else self.base_url
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Accept": "application/json",
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_connection(self) -> dict:
|
||||
"""Return Tandoor version info or raise on failure."""
|
||||
# Try the recipe list endpoint as a connection test
|
||||
r = self.session.get(
|
||||
f"{self.api_url}/api/recipe/",
|
||||
params={"limit": 1},
|
||||
timeout=10,
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
# Try to get version from openapi endpoint
|
||||
version = "unknown"
|
||||
try:
|
||||
r2 = self.session.get(f"{self.api_url}/openapi/", timeout=10)
|
||||
if r2.ok:
|
||||
# Parse version from YAML: "version: 1.5.26"
|
||||
m = re.search(r"version:\s*['\"]?([0-9][0-9a-z._-]*)", r2.text)
|
||||
if m:
|
||||
version = m.group(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"version": version}
|
||||
|
||||
def find_duplicate(self, url: str, title: str = "") -> dict | None:
|
||||
"""Check if a recipe with this source URL already exists."""
|
||||
if not url and not title:
|
||||
return None
|
||||
search = title or url.split("/")[-1].replace("-", " ")
|
||||
r = self.session.get(
|
||||
f"{self.api_url}/api/recipe/",
|
||||
params={"query": search, "limit": 20},
|
||||
timeout=10,
|
||||
)
|
||||
if not r.ok:
|
||||
return None
|
||||
for item in r.json().get("results", []):
|
||||
source = item.get("source_url") or ""
|
||||
desc = item.get("description") or ""
|
||||
if source == url or url in desc:
|
||||
return {
|
||||
"id": item["id"],
|
||||
"name": item["name"],
|
||||
"url": f"{self.base_url}/view/recipe/{item['id']}",
|
||||
}
|
||||
return None
|
||||
|
||||
def create_recipe(self, recipe: dict) -> dict:
|
||||
"""Create a recipe in Tandoor from a scraper result dict.
|
||||
|
||||
Returns {"id": int, "url": str}.
|
||||
"""
|
||||
payload = self._build_payload(recipe)
|
||||
r = self.session.post(
|
||||
f"{self.api_url}/api/recipe/",
|
||||
json=payload,
|
||||
timeout=15,
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
recipe_id = data["id"]
|
||||
|
||||
# Upload image if available
|
||||
image_url = recipe.get("image_url")
|
||||
if image_url:
|
||||
try:
|
||||
self._upload_image(recipe_id, image_url)
|
||||
except Exception:
|
||||
pass # non-fatal
|
||||
|
||||
return {
|
||||
"id": recipe_id,
|
||||
"url": f"{self.base_url}/view/recipe/{recipe_id}",
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_payload(self, recipe: dict) -> dict:
|
||||
description = recipe.get("description", "")
|
||||
original_url = recipe.get("original_url", "")
|
||||
if original_url and original_url not in description:
|
||||
description = f"{description}\n\n{original_url}".strip()
|
||||
|
||||
# Build ingredients list
|
||||
ingredients = []
|
||||
order = 0
|
||||
pending_group = ""
|
||||
for item in recipe.get("ingredients", []):
|
||||
if isinstance(item, dict):
|
||||
if "group" in item and "food" not in item:
|
||||
# Group header
|
||||
ingredients.append({
|
||||
"food": None,
|
||||
"unit": None,
|
||||
"amount": 0,
|
||||
"note": item["group"],
|
||||
"is_header": True,
|
||||
"no_amount": True,
|
||||
"order": order,
|
||||
})
|
||||
order += 1
|
||||
else:
|
||||
ingredients.append(self._build_ingredient(item, order))
|
||||
order += 1
|
||||
|
||||
# Build steps — all ingredients on step 0
|
||||
instructions = recipe.get("instructions", [])
|
||||
steps = []
|
||||
for i, text in enumerate(instructions):
|
||||
steps.append({
|
||||
"instruction": text,
|
||||
"ingredients": ingredients if i == 0 else [],
|
||||
"order": i,
|
||||
})
|
||||
|
||||
if not instructions and ingredients:
|
||||
steps.append({
|
||||
"instruction": "",
|
||||
"ingredients": ingredients,
|
||||
"order": 0,
|
||||
})
|
||||
|
||||
return {
|
||||
"name": recipe["title"],
|
||||
"description": description,
|
||||
"source_url": original_url,
|
||||
"steps": steps,
|
||||
"servings": 1,
|
||||
}
|
||||
|
||||
def _build_ingredient(self, item: dict, order: int) -> dict:
|
||||
"""Build a Tandoor ingredient from a structured dict."""
|
||||
qty_str = str(item.get("quantity", "")).strip()
|
||||
unit_str = item.get("unit", "").strip()
|
||||
food_str = item.get("food", "").strip()
|
||||
extra = item.get("extra", "").strip()
|
||||
|
||||
has_structured = bool(qty_str or unit_str)
|
||||
|
||||
if has_structured and food_str:
|
||||
qty = 0
|
||||
try:
|
||||
qty = float(qty_str.replace(",", "."))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"food": {"name": food_str},
|
||||
"unit": {"name": unit_str} if unit_str else None,
|
||||
"amount": qty,
|
||||
"note": extra,
|
||||
"is_header": False,
|
||||
"no_amount": False,
|
||||
"order": order,
|
||||
}
|
||||
else:
|
||||
# No quantity/unit — food-only or note
|
||||
note = food_str
|
||||
if extra:
|
||||
note = f"{note} ({extra})" if note else extra
|
||||
if food_str:
|
||||
return {
|
||||
"food": {"name": food_str},
|
||||
"unit": None,
|
||||
"amount": 0,
|
||||
"note": extra,
|
||||
"is_header": False,
|
||||
"no_amount": True,
|
||||
"order": order,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"food": None,
|
||||
"unit": None,
|
||||
"amount": 0,
|
||||
"note": note,
|
||||
"is_header": False,
|
||||
"no_amount": True,
|
||||
"order": order,
|
||||
}
|
||||
|
||||
def _upload_image(self, recipe_id: int, image_url: str):
|
||||
"""Download image from *image_url* and upload it to the recipe."""
|
||||
img_resp = requests.get(image_url, timeout=30, headers={
|
||||
"User-Agent": "RecipeImporter/1.0",
|
||||
})
|
||||
img_resp.raise_for_status()
|
||||
|
||||
content_type = img_resp.headers.get("Content-Type", "image/jpeg")
|
||||
ext = "jpg"
|
||||
if "png" in content_type:
|
||||
ext = "png"
|
||||
elif "webp" in content_type:
|
||||
ext = "webp"
|
||||
|
||||
files = {
|
||||
"image": (f"recipe.{ext}", io.BytesIO(img_resp.content), content_type),
|
||||
}
|
||||
r = self.session.put(
|
||||
f"{self.api_url}/api/recipe/{recipe_id}/image/",
|
||||
files=files,
|
||||
timeout=30,
|
||||
)
|
||||
r.raise_for_status()
|
||||
+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