From 7f3645f4a8e0310392f6b138ac01c995ceb3c666 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Wed, 9 Sep 2026 15:46:00 +0200 Subject: [PATCH] API - DELETE for Watch history #4397 (#4403) --- changedetectionio/api/Watch.py | 14 ++++++ changedetectionio/tests/test_api.py | 78 +++++++++++++++++++++++++++++ docs/api-spec.yaml | 36 ++++++++++++- docs/api_v1/index.html | 30 ++++++++--- 4 files changed, 151 insertions(+), 7 deletions(-) diff --git a/changedetectionio/api/Watch.py b/changedetectionio/api/Watch.py index 377e8d161..8de0900e5 100644 --- a/changedetectionio/api/Watch.py +++ b/changedetectionio/api/Watch.py @@ -293,6 +293,20 @@ class WatchHistory(Resource): abort(404, message='No watch exists with the UUID of {}'.format(uuid)) return watch.history, 200 + # Delete all history/snapshots for a watch, but keep the watch itself + # curl -X DELETE http://localhost:5000/api/v1/watch//history + @auth.check_token + @validate_openapi_request('deleteWatchHistory') + def delete(self, uuid): + """Clear all snapshot history for a watch (the watch itself is kept).""" + if not self.datastore.data['watching'].get(uuid): + abort(404, message='No watch exists with the UUID of {}'.format(uuid)) + + # Same call as the UI "Clear history" button - wipes snapshots/screenshots and + # resets last_checked etc, while preserving the watch and its processor config + self.datastore.clear_watch_history(uuid) + return 'OK', 204 + class WatchSingleHistory(Resource): def __init__(self, **kwargs): diff --git a/changedetectionio/tests/test_api.py b/changedetectionio/tests/test_api.py index abe8083ac..178022376 100644 --- a/changedetectionio/tests/test_api.py +++ b/changedetectionio/tests/test_api.py @@ -338,6 +338,84 @@ def test_api_simple(client, live_server, measure_memory_usage, datastore_path): ) assert len(res.json) == 0, "Watch list should be empty" +def test_api_delete_watch_history(client, live_server, measure_memory_usage, datastore_path): + """DELETE /api/v1/watch//history should wipe the snapshots but keep the watch (#4397)""" + + api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') + + set_original_response(datastore_path=datastore_path) + test_url = url_for('test_endpoint', _external=True) + + res = client.post( + url_for("createwatch"), + data=json.dumps({"url": test_url}), + headers={'content-type': 'application/json', 'x-api-key': api_key}, + follow_redirects=True + ) + assert res.status_code == 201 + watch_uuid = res.json.get('uuid') + wait_for_all_checks(client) + + # A second snapshot so we know we're clearing more than one + set_modified_response(datastore_path=datastore_path) + client.get(url_for("watch", uuid=watch_uuid, recheck='1'), headers={'x-api-key': api_key}) + wait_for_all_checks(client) + + res = client.get( + url_for("watchhistory", uuid=watch_uuid), + headers={'x-api-key': api_key}, + ) + assert len(res.json) == 2, "Should have two history entries before clearing" + + # Unknown watch UUID should 404 and not blow up + res = client.delete( + url_for("watchhistory", uuid='4d8b5b4a-8e0b-4d4a-9f57-3f2b1c0d9e11'), + headers={'x-api-key': api_key}, + ) + assert res.status_code == 404 + + # Requires the API key + res = client.delete(url_for("watchhistory", uuid=watch_uuid)) + assert res.status_code == 403 + + # Pause it first - clearing resets last_checked to 0 which otherwise makes the ticker + # queue an instant recheck, and that would race with the assertions below + client.get(url_for("watch", uuid=watch_uuid, paused='paused'), headers={'x-api-key': api_key}) + + # Now really clear it + res = client.delete( + url_for("watchhistory", uuid=watch_uuid), + headers={'x-api-key': api_key}, + ) + assert res.status_code == 204 + + res = client.get( + url_for("watchhistory", uuid=watch_uuid), + headers={'x-api-key': api_key}, + ) + assert res.json == {}, "History should be empty after DELETE" + + # The watch itself must survive, with its state reset + res = client.get( + url_for("watch", uuid=watch_uuid), + headers={'x-api-key': api_key} + ) + assert res.status_code == 200 + assert res.json.get('url') == test_url + assert res.json.get('history_n') == 0 + assert res.json.get('last_checked') == 0 + assert res.json.get('previous_md5') == False + + # And a snapshot fetch now has nothing to give + res = client.get( + url_for("watchsinglehistory", uuid=watch_uuid, timestamp='latest'), + headers={'x-api-key': api_key}, + ) + assert res.status_code == 404 + + delete_all_watches(client) + + def test_roundtrip_API(client, live_server, measure_memory_usage, datastore_path): """ Test the full round trip, this way we test the default Model fits back into OpenAPI spec diff --git a/docs/api-spec.yaml b/docs/api-spec.yaml index 1b81cba26..b005d21a8 100644 --- a/docs/api-spec.yaml +++ b/docs/api-spec.yaml @@ -28,7 +28,7 @@ info: For example: `x-api-key: YOUR_API_KEY` - version: 0.1.8 + version: 0.1.9 contact: name: ChangeDetection.io url: https://github.com/dgtlmoon/changedetection.io @@ -1271,6 +1271,40 @@ paths: '404': description: Web page change monitor (watch) not found + delete: + operationId: deleteWatchHistory + tags: [Watch History] + summary: Delete watch history + description: | + Delete/clear all historical snapshots for a web page change monitor (watch), the watch itself is kept. + This also resets the "last checked"/"last changed" state, so the watch will be re-checked on the next cycle. + x-code-samples: + - lang: 'curl' + source: | + curl -X DELETE "http://localhost:5000/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/history" \ + -H "x-api-key: YOUR_API_KEY" + - lang: 'Python' + source: | + import requests + + headers = {'x-api-key': 'YOUR_API_KEY'} + uuid = '095be615-a8ad-4c33-8e9c-c7612fbf6c9f' + response = requests.delete(f'http://localhost:5000/api/v1/watch/{uuid}/history', headers=headers) + print(response.status_code) + parameters: + - name: uuid + in: path + required: true + description: Web page change monitor (watch) unique ID + schema: + type: string + format: uuid + responses: + '204': + description: History deleted successfully + '404': + description: Web page change monitor (watch) not found + /watch/{uuid}/history/{timestamp}: get: operationId: getWatchSnapshot diff --git a/docs/api_v1/index.html b/docs/api_v1/index.html index a76a1e7a9..ef6820ed6 100644 --- a/docs/api_v1/index.html +++ b/docs/api_v1/index.html @@ -440,7 +440,7 @@ data-styled.g138[id="sc-enPhjR"]{content:"SikXG,"}/*!sc*/ -

ChangeDetection.io API (0.1.8)

Download OpenAPI specification:

ChangeDetection.io Web page monitoring and notifications API

ChangeDetection.io API (0.1.9)

Download OpenAPI specification:

ChangeDetection.io Web page monitoring and notifications API

REST API for managing Page watches, Group tags, and Notifications.

changedetection.io can be driven by its built in simple API, in the examples below you will also find curl command line and python examples to help you get started faster.

@@ -892,7 +892,25 @@ as the query argument for fetching a single watch history snapshot.

" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">

Custom server

{protocol}://{host}/api/v1/watch/{uuid}/history

Request samples

curl -X GET "http://localhost:5000/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/history" \
   -H "x-api-key: YOUR_API_KEY"
-

Response samples

Content type
application/json
{
  • "1640995200": "/path/to/snapshot1.txt",
  • "1640998800": "/path/to/snapshot2.txt"
}

Get the difference between two snapshots

Response samples

Content type
application/json
{
  • "1640995200": "/path/to/snapshot1.txt",
  • "1640998800": "/path/to/snapshot2.txt"
}

Delete watch history

Delete/clear all historical snapshots for a web page change monitor (watch), the watch itself is kept. +This also resets the "last checked"/"last changed" state, so the watch will be re-checked on the next cycle.

+
Authorizations:
ApiKeyAuth
path Parameters
uuid
required
string <uuid>

Web page change monitor (watch) unique ID

+

Responses

Request samples

curl -X DELETE "http://localhost:5000/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/history" \
+  -H "x-api-key: YOUR_API_KEY"
+

Get the difference between two snapshots

Production server

https://yourdomain.com/api/v1/watch/{uuid}/difference/{from_timestamp}/{to_timestamp}

Custom server

-
{protocol}://{host}/api/v1/watch/{uuid}/difference/{from_timestamp}/{to_timestamp}

Request samples

# Compare previous snapshot to latest with colored HTML
+
{protocol}://{host}/api/v1/watch/{uuid}/difference/{from_timestamp}/{to_timestamp}

Request samples

# Compare previous snapshot to latest with colored HTML
 curl -X GET "http://localhost:5000/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/difference/previous/latest?format=htmlcolor" \
   -H "x-api-key: YOUR_API_KEY"
 
@@ -1051,7 +1069,7 @@ curl -X GET "http
 # Show only additions (hide removed/replaced content), ignore whitespace
 curl -X GET "http://localhost:5000/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f/difference/previous/latest?format=htmlcolor&removed=false&replaced=false&ignoreWhitespace=true" \
   -H "x-api-key: YOUR_API_KEY"
-

Snapshots

Snapshots

-X GET "http://localhost:5000/api/v1/full-spec"