diff --git a/changedetectionio/blueprint/ui/preview.py b/changedetectionio/blueprint/ui/preview.py
index 40d5e5808..f61e048d8 100644
--- a/changedetectionio/blueprint/ui/preview.py
+++ b/changedetectionio/blueprint/ui/preview.py
@@ -57,7 +57,6 @@ def construct_blueprint(datastore: ChangeDetectionStore):
# Fallback: if processor doesn't have preview module, use default text preview
content = []
- versions = []
timestamp = None
extra_stylesheets = [url_for('static_content', group='styles', filename='diff.css')]
@@ -70,20 +69,22 @@ def construct_blueprint(datastore: ChangeDetectionStore):
ignored_line_numbers = []
blocked_line_numbers = []
- if datastore.data['watching'][uuid].history_n == 0 and (watch.get_error_text() or watch.get_error_snapshot()):
+ # Gate on whether there is a snapshot to show, NOT on whether an error was recorded: a
+ # watch that has never completed a check and has no error text either (freshly added, or
+ # paused before its first check) has nothing to index into, and asking for the newest of
+ # no versions used to be a 500 on a page reachable from the watch list.
+ versions = list(watch.history.keys())
+ if not versions:
flash(gettext("Preview unavailable - No fetch/check completed or triggers not reached"), "error")
else:
# So prepare the latest preview or not
preferred_version = request.values.get('version') if request.method == 'POST' else request.args.get('version')
-
- versions = list(watch.history.keys())
timestamp = versions[-1]
if preferred_version and preferred_version in versions:
timestamp = preferred_version
try:
- versions = list(watch.history.keys())
content = watch.get_history_snapshot(timestamp=timestamp)
triggered_line_numbers = html_tools.strip_ignore_text(content=content,
diff --git a/changedetectionio/tests/test_preview_endpoints.py b/changedetectionio/tests/test_preview_endpoints.py
index 6715685fa..1e4656c8b 100644
--- a/changedetectionio/tests/test_preview_endpoints.py
+++ b/changedetectionio/tests/test_preview_endpoints.py
@@ -66,3 +66,21 @@ def test_fetch_pdf(client, live_server, measure_memory_usage, datastore_path):
assert changed_md5.encode('utf-8') in res.data
assert b'here is a change' in res.data
+
+
+def test_preview_of_a_watch_with_no_history(client, live_server, measure_memory_usage, datastore_path):
+ """/preview must not 500 on a watch that has never completed a check.
+
+ The page is linked from the watch list, so it is reachable for a freshly added or paused
+ watch. The old guard only skipped the history lookup when history_n == 0 AND an error had
+ been recorded, so a clean never-checked watch asked for the newest of no versions and the
+ request died with IndexError.
+ """
+ datastore = client.application.config.get('DATASTORE')
+ uuid = datastore.add_watch(url="https://example.com", extras={'paused': True})
+ assert datastore.data['watching'][uuid].history_n == 0
+ assert not datastore.data['watching'][uuid].get_error_text()
+
+ res = client.get(url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True)
+ assert res.status_code == 200
+ assert b'Preview unavailable' in res.data
diff --git a/docs/api_v1/index.html b/docs/api_v1/index.html
index ef6820ed6..e92b666eb 100644
--- a/docs/api_v1/index.html
+++ b/docs/api_v1/index.html
@@ -288,8 +288,6 @@ data-styled.g66[id="sc-dTWiOz"]{content:"kMQdIk,"}/*!sc*/
.bDfgbe{border-radius:2px;background-color:rgba(104,104,207,0.05);color:rgba(50,50,159,0.9);margin:0 5px;padding:0 5px;border:1px solid rgba(50,50,159,0.1);}/*!sc*/
+{margin-left:0;}/*!sc*/
data-styled.g68[id="sc-goiVcJ"]{content:"bDfgbe,"}/*!sc*/
-.jBrfIx{background-color:transparent;border:0;color:#666;margin-left:5px;border-radius:2px;cursor:pointer;outline-color:#666;font-size:12px;}/*!sc*/
-data-styled.g69[id="sc-gSifMm"]{content:"jBrfIx,"}/*!sc*/
.eA-DYPM{margin:0 5px;vertical-align:text-top;}/*!sc*/
data-styled.g75[id="sc-bBhMX"]{content:"eA-DYPM,"}/*!sc*/
.hRtRoN:after{content:' and ';font-weight:normal;}/*!sc*/
@@ -516,6 +514,12 @@ notification preferences, and content filtering options.
Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
url
required
string <uri> <= 5000 characters
URL to monitor for changes
+
link_to_open
string <= 5000 characters
Optional human-facing URL to open instead of url. Use when url points at something
+that is not meant to be read in a browser (an API endpoint or RSS feed, for example).
+An empty string means "use url". Exposed rendered as open_link.
title
string or null <= 5000 characters
Custom title for the web page change monitor (watch), not to be confused with page_title
system (default) - follow the global Default browser
+
a built-in engine name, e.g. html_requests (fast plain HTTP client),
+html_webdriver (Playwright/Puppeteer/Selenium, depending on configuration) or
+html_external_cdp (a browser you run elsewhere, reached over CDP)
+
a plugin-provided engine, if one is installed
+
the id of a saved browser from the Browsers page, which is also what a pre-0.61
+extra_browser_<name> endpoint becomes when it is migrated there
object
HTTP headers to include in requests
@@ -696,12 +706,18 @@ No watch was created - delete one to make room.
" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">
Custom server
{protocol}://{host}/api/v1/watch/{uuid}
Request samples
curl
Python
curl -X GET "http://localhost:5000/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f" \
-H "x-api-key: YOUR_API_KEY"
-
Update an existing web page change monitor (watch) using JSON. Accepts the same structure as returned in get single watch information.
Authorizations:
ApiKeyAuth
path Parameters
uuid
required
string <uuid>
Web page change monitor (watch) unique ID
Request Body schema: application/json
required
url
string <uri> <= 5000 characters
URL to monitor for changes
+
link_to_open
string <= 5000 characters
Optional human-facing URL to open instead of url. Use when url points at something
+that is not meant to be read in a browser (an API endpoint or RSS feed, for example).
+An empty string means "use url". Exposed rendered as open_link.
title
string or null <= 5000 characters
Custom title for the web page change monitor (watch), not to be confused with page_title
system (default) - follow the global Default browser
+
a built-in engine name, e.g. html_requests (fast plain HTTP client),
+html_webdriver (Playwright/Puppeteer/Selenium, depending on configuration) or
+html_external_cdp (a browser you run elsewhere, reached over CDP)
+
a plugin-provided engine, if one is installed
+
the id of a saved browser from the Browsers page, which is also what a pre-0.61
+extra_browser_<name> endpoint becomes when it is migrated there
object
HTTP headers to include in requests
@@ -858,7 +880,7 @@ On a tag/group it is ternary and is that group's only AI control:
" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">
Optional human-facing URL to open instead of url. Use when url points at something
+that is not meant to be read in a browser (an API endpoint or RSS feed, for example).
+An empty string means "use url". Exposed rendered as open_link.
title
required
string or null <= 5000 characters
Custom title for the web page change monitor (watch), not to be confused with page_title
system (default) - follow the global Default browser
+
a built-in engine name, e.g. html_requests (fast plain HTTP client),
+html_webdriver (Playwright/Puppeteer/Selenium, depending on configuration) or
+html_external_cdp (a browser you run elsewhere, reached over CDP)
+
a plugin-provided engine, if one is installed
+
the id of a saved browser from the Browsers page, which is also what a pre-0.61
+extra_browser_<name> endpoint becomes when it is migrated there
object
HTTP headers to include in requests
@@ -1344,12 +1378,18 @@ Leave empty to use the colour auto-generated from the tag name.
" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">
Custom server
{protocol}://{host}/api/v1/tag/{uuid}
Request samples
curl
Python
curl -X GET "http://localhost:5000/api/v1/tag/550e8400-e29b-41d4-a716-446655440000" \
-H "x-api-key: YOUR_API_KEY"
-
Optional human-facing URL to open instead of url. Use when url points at something
+that is not meant to be read in a browser (an API endpoint or RSS feed, for example).
+An empty string means "use url". Exposed rendered as open_link.
title
string or null <= 5000 characters
Custom title for the web page change monitor (watch), not to be confused with page_title
system (default) - follow the global Default browser
+
a built-in engine name, e.g. html_requests (fast plain HTTP client),
+html_webdriver (Playwright/Puppeteer/Selenium, depending on configuration) or
+html_external_cdp (a browser you run elsewhere, reached over CDP)
+
a plugin-provided engine, if one is installed
+
the id of a saved browser from the Browsers page, which is also what a pre-0.61
+extra_browser_<name> endpoint becomes when it is migrated there
object
HTTP headers to include in requests
@@ -1530,7 +1576,7 @@ Leave empty to use the colour auto-generated from the tag name.
" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">