Ability to have a preview of the 'processor' output in add-watch-ui/

This commit is contained in:
dgtlmoon
2026-07-22 16:05:55 +02:00
parent 995560ad4f
commit ea6201bcc9
22 changed files with 461 additions and 1 deletions
@@ -142,10 +142,23 @@ def construct_blueprint(datastore: ChangeDetectionStore):
logger.error(f"Add-watch snapshot: could not park temporary data for {url}: {e}")
temp_uuid = None
# Optional per-processor previews of what each would read off this page (e.g. restock shows
# the detected price/stock). Computed once here from the fetched HTML for every processor
# offered on the page; the client shows the one for the selected processor and swaps on
# change. Only processors that implement the hook and return something are included.
previews = {}
if html:
from changedetectionio import processors
for pname, _ in processors.available_processors(processor_filter={'supports_visual_selector': True}):
preview = processors.get_processor_preview(datastore, pname, html, url=url)
if preview:
previews[pname] = preview
return jsonify({
"temporary_uuid": temp_uuid,
"screenshot": f"data:image/jpeg;base64,{base64.b64encode(screenshot).decode('ascii')}",
"xpath_data": xpath_data,
"processor_previews": previews,
})
return add_watch_ui_blueprint
@@ -13,6 +13,16 @@ $(document).ready(() => {
const $clear = $('#clear-selector');
const $includeFilters = $('#include_filters');
const $temporaryUuid = $('#temporary_uuid');
const $processorPreview = $('#processor-add-watch-ui-preview-text');
// Per-processor previews from the last snapshot ({processor_name: "line to show"}).
let processorPreviews = {};
function renderProcessorPreview() {
const selected = $('input[name="processor"]:checked').val();
const text = selected ? processorPreviews[selected] : null;
$processorPreview.text(text || '').toggle(!!text);
}
const vs = window.initVisualSelector({
$canvas: $('#selector-canvas'),
@@ -50,6 +60,8 @@ $(document).ready(() => {
showState('loading');
// A previous parked snapshot is now stale; drop it until this fetch succeeds.
$temporaryUuid.val('');
processorPreviews = {};
renderProcessorPreview();
// Preview with whichever interactive browser the user picked (defaults to the checked one).
const browser = $('input[name="fetch_backend"]:checked').val() || '';
@@ -61,6 +73,8 @@ $(document).ready(() => {
}).done((data) => {
showState('ready');
$temporaryUuid.val(data.temporary_uuid || '');
processorPreviews = data.processor_previews || {};
renderProcessorPreview();
vs.load({screenshotSrc: data.screenshot, xpathData: data.xpath_data});
}).fail((xhr) => {
const msg = (xhr && xhr.responseText) ? xhr.responseText : 'Could not fetch a preview for that URL.';
@@ -69,6 +83,9 @@ $(document).ready(() => {
});
}
// Swap the preview line when the processor selection changes (no re-fetch needed).
$(document).on('change', 'input[name="processor"]', renderProcessorPreview);
$go.on('click', fetchSnapshot);
// Enter in the URL box should fetch a preview, not submit the whole form
@@ -69,6 +69,10 @@
<div class="add-watch-option-group" id="quick-watch-processor-type">
{{ render_simple_field(form.processor) }}
<!-- Per-processor preview of what it would read off the fetched page
(e.g. restock shows the detected price/stock); filled in by add-watch.js
from the snapshot response, and swapped when the processor changes. -->
<div id="processor-add-watch-ui-preview-text" style="display: none;"></div>
</div>
<div class="add-watch-option-group" id="by-element-toggle-group">
+21
View File
@@ -276,6 +276,27 @@ def _processor_capability(name, capability, default=False):
return default
def get_processor_preview(datastore, name, html_content, url=None):
"""Run a processor's OPTIONAL Add-Watch-UI preview and return a short human string (or None).
A processor opts in by overriding `add_watch_ui_processor_preview(self, html_content, url=None)`
on its `perform_site_check` class (default on the base ABC returns None). We instantiate it with
no watch (watch_uuid=None is tolerated) since the preview runs against the raw fetched HTML, not
a saved watch. Best-effort UI sugar: a missing override, an error, or a None/empty return all
yield None so the caller shows nothing for that processor. Never raises.
"""
module = get_processor_module(name)
if not module or not hasattr(module, 'perform_site_check'):
return None
try:
handler = module.perform_site_check(datastore=datastore, watch_uuid=None)
preview = handler.add_watch_ui_processor_preview(html_content=html_content, url=url)
return preview or None
except Exception as e:
logger.debug(f"Processor '{name}' add-watch-ui preview failed: {e}")
return None
def available_processors(processor_filter=None):
"""
Get a list of processors by name and description for the UI elements.
+11
View File
@@ -97,6 +97,17 @@ class difference_detection_processor():
logger.warning(f"Failed to read checksum file for {self.watch_uuid}: {e}")
self.last_raw_content_checksum = None
def add_watch_ui_processor_preview(self, html_content, url=None):
"""Optional Add-Watch-UI preview hook.
Return a short human string describing what THIS processor would read off the just-fetched
page (shown under the processor picker on /add-watch-ui, so the user can see the processor
"works" before saving), or None for no preview. Best-effort UI sugar - the caller swallows
exceptions and treats None/empty as "show nothing". Override in a processor to opt in; the
default is no preview. Runs against the raw fetched HTML, so it does not need a saved watch.
"""
return None
async def validate_iana_url(self):
"""Pre-flight SSRF check — runs DNS lookup in executor to avoid blocking the event loop.
Covers all fetchers (requests, playwright, puppeteer, plugins) since every fetch goes
@@ -404,6 +404,32 @@ class perform_site_check(difference_detection_processor):
screenshot = None
xpath_data = None
def add_watch_ui_processor_preview(self, html_content, url=None):
"""Add-Watch-UI preview: reuse the exact price/availability extraction the real check uses,
so the line shown under the processor picker matches what a saved watch would detect."""
from flask_babel import gettext
try:
r = extract_itemprop_availability_safe(html_content)
except MoreThanOnePriceFound:
return gettext("More than one price detected on this page - restock/price detection needs a single-product page.")
except Exception:
return None
price = r.get('price')
availability = r.get('availability')
if price is None and not availability:
return gettext("No price or stock information was detected on this page.")
parts = []
if price is not None:
currency = r.get('currency') or ''
parts.append(gettext("price: %(price)s", price=f"{currency}{price}".strip()))
if availability:
# availability is often a schema.org URL (e.g. https://schema.org/InStock) - show the tail.
parts.append(gettext("availability: %(availability)s",
availability=str(availability).rstrip('/').split('/')[-1]))
return gettext("Detected — %(details)s", details=", ".join(parts))
def run_changedetection(self, watch, force_reprocess=False):
import hashlib
@@ -4147,6 +4147,29 @@ msgstr "Doplnění zásob a zjištění ceny pro stránky s JEDINÝM produktem"
msgid "Detects if the product goes back to in-stock"
msgstr "Zjistí, zda se produkt vrátí na sklad"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4199,6 +4199,29 @@ msgstr "Wiederauffüllung und Preiserkennung für Seiten mit einem EINZELNEN Pro
msgid "Detects if the product goes back to in-stock"
msgstr "Erkennt, ob das Produkt wieder auf Lager ist"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4139,6 +4139,29 @@ msgstr ""
msgid "Detects if the product goes back to in-stock"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4139,6 +4139,29 @@ msgstr ""
msgid "Detects if the product goes back to in-stock"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4212,6 +4212,29 @@ msgstr "Reabastecimiento y detección de precios para páginas con un ÚNICO pro
msgid "Detects if the product goes back to in-stock"
msgstr "Detecta si el producto vuelve a estar en stock"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4152,6 +4152,29 @@ msgstr "Détection de réapprovisionnement et de prix pour les pages avec un SEU
msgid "Detects if the product goes back to in-stock"
msgstr "Détecte si le produit revient en stock"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4141,6 +4141,29 @@ msgstr "Rilevamento disponibilità e prezzi per pagine con UN SINGOLO prodotto"
msgid "Detects if the product goes back to in-stock"
msgstr "Rileva se il prodotto torna disponibile"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4158,6 +4158,29 @@ msgstr "単一製品ページの在庫補充&価格検知"
msgid "Detects if the product goes back to in-stock"
msgstr "製品が在庫ありに戻ったかどうかを検知します"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4149,6 +4149,29 @@ msgstr "단일 제품이 포함된 페이지의 재입고 및 가격 감지"
msgid "Detects if the product goes back to in-stock"
msgstr "제품이 다시 재고 있음 상태가 되었는지 감지합니다."
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
+24 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: changedetection.io 0.55.8\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-07-22 10:10+0200\n"
"POT-Creation-Date: 2026-07-22 15:56+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -4138,6 +4138,29 @@ msgstr ""
msgid "Detects if the product goes back to in-stock"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4189,6 +4189,29 @@ msgstr "Detecção de Estoque e Preço para páginas com um ÚNICO produto"
msgid "Detects if the product goes back to in-stock"
msgstr "Detecta se o produto volta ao estoque"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4255,6 +4255,29 @@ msgstr "Пополнение запасов и определение цен д
msgid "Detects if the product goes back to in-stock"
msgstr "Определяет, возвращается ли товар на склад"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4192,6 +4192,29 @@ msgstr "TEK bir ürüne sahip sayfalar için Yeniden Stoklama ve Fiyat tespiti"
msgid "Detects if the product goes back to in-stock"
msgstr "Ürünün tekrar stoka girip girmediğini tespit eder"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4171,6 +4171,29 @@ msgstr "Виявлення поповнення та ціни для сторі
msgid "Detects if the product goes back to in-stock"
msgstr "Визначає, чи повернувся товар у наявність"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4145,6 +4145,29 @@ msgstr "适用于单一商品页面的补货与价格检测"
msgid "Detects if the product goes back to in-stock"
msgstr "检测商品是否恢复有库存"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""
@@ -4145,6 +4145,29 @@ msgstr "針對單一產品頁面的補貨與價格檢測"
msgid "Detects if the product goes back to in-stock"
msgstr "檢測產品是否恢復庫存"
#: changedetectionio/processors/restock_diff/processor.py
msgid "More than one price detected on this page - restock/price detection needs a single-product page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
msgid "No price or stock information was detected on this page."
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "price: %(price)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "availability: %(availability)s"
msgstr ""
#: changedetectionio/processors/restock_diff/processor.py
#, python-format
msgid "Detected — %(details)s"
msgstr ""
#: changedetectionio/processors/restock_diff/templates/restock_diff/difference.html
msgid "Price & stock history"
msgstr ""