Compare commits

...

2 Commits

Author SHA1 Message Date
dgtlmoon 0d7eb0de71 status fix 2026-05-12 14:34:27 +02:00
dgtlmoon 33c150fe7b UI - Make LLM status sticky 2026-05-12 13:14:23 +02:00
5 changed files with 45 additions and 5 deletions
@@ -356,7 +356,7 @@ window.watchOverviewI18n = {
{#last_checked becomes fetch-start-time#}
<td class="last-checked" data-timestamp="{{ watch.last_checked }}" data-fetchduration={{ watch.fetch_time }} data-eta_complete="{{ watch.last_checked+watch.fetch_time }}" data-label="{{ _('Last Checked') }}">
<div class="spinner-wrapper" style="display:none;" >
<span class="spinner"></span><span class="status-text">&nbsp;{{ _('Checking now') }}</span>
<span class="spinner"></span><span class="status-text">&nbsp;{{ watch['__check_status'] or _('Checking now') }}</span>
</div>
<span class="innertext">{{watch|format_last_checked_time|safe}}</span>
</td>
+3
View File
@@ -49,6 +49,9 @@ def completion(model: str, messages: list, api_key: str = None,
_retryable = (litellm.Timeout, litellm.APIConnectionError)
logger.trace("Sending payload to LLM.. ")
logger.trace(messages)
for attempt in range(1, DEFAULT_RETRIES + 1):
try:
response = litellm.completion(**kwargs)
+6 -2
View File
@@ -1064,6 +1064,7 @@ class model(EntityPersistenceMixin, watch_base):
Prepare watch data for commit.
Excludes processor_config_* keys (stored in separate files).
Excludes __-prefixed keys (transient in-memory state — must not persist to disk).
Normalizes browser_steps to empty list if no meaningful steps.
"""
import copy
@@ -1077,8 +1078,11 @@ class model(EntityPersistenceMixin, watch_base):
else:
snapshot = dict(self)
# Exclude processor config keys (stored separately)
watch_dict = {k: copy.deepcopy(v) for k, v in snapshot.items() if not k.startswith('processor_config_')}
# Exclude processor config keys (stored separately) and __-prefixed transient keys
watch_dict = {
k: copy.deepcopy(v) for k, v in snapshot.items()
if not k.startswith('processor_config_') and not k.startswith('__')
}
# Normalize browser_steps: if no meaningful steps, save as empty list
if not self.has_browser_steps:
+7
View File
@@ -335,6 +335,13 @@ class watch_base(dict):
if self.__watch_was_edited:
return # Already marked as edited
# __-prefixed keys are transient in-memory state (e.g. __check_status set by
# set_watch_minitext_status). They never persist to disk and must not trigger
# the edited flag — otherwise just observing a check in progress would force
# the next run to bypass the unchanged-content skip.
if isinstance(key, str) and key.startswith('__'):
return
# Import from shared schema utilities (no circular dependency)
from .schema_utils import get_readonly_watch_fields
readonly_fields = get_readonly_watch_fields()
+28 -2
View File
@@ -9,9 +9,16 @@ from changedetectionio.pluggy_interface import apply_update_handler_alter, apply
import asyncio
import os
import re
import sys
import time
# Allow alphanumerics, space, and a small set of punctuation that appears in legitimate
# status strings ("Querying AI/LLM (intent)..", "Fetching page.."). Anything that could
# be HTML-active (<, >, &, ", ', =, ;, {, }, `, \) is stripped.
_MINITEXT_STATUS_SAFE_RE = re.compile(r'[^A-Za-z0-9 ().,/:\-]')
_MINITEXT_STATUS_MAX_LEN = 80
from loguru import logger
# Async version of update_worker
@@ -20,6 +27,22 @@ from loguru import logger
IN_PYTEST = "pytest" in sys.modules or "PYTEST_CURRENT_TEST" in os.environ
DEFER_SLEEP_TIME_ALREADY_QUEUED = 0.3 if IN_PYTEST else 10.0
def set_watch_minitext_status(watch, status):
"""
Set a transient status line for a watch (e.g. "Fetching page..", "Querying AI/LLM..").
Writes to watch['__check_status'] so a client reloading the page can render the
last known status, and fires the realtime signal so already-connected clients
update live. __-prefixed key is filtered from disk by Watch._get_commit_data().
Status is sanitized to alphanumerics, space, and safe punctuation only.
"""
safe_status = _MINITEXT_STATUS_SAFE_RE.sub('', str(status))[:_MINITEXT_STATUS_MAX_LEN]
watch['__check_status'] = safe_status
signal('watch_small_status_comment').send(watch_uuid=watch['uuid'], status=safe_status)
async def async_update_worker(worker_id, q, notification_q, app, datastore, executor=None):
"""
Async worker function that processes watch check jobs from the queue.
@@ -159,8 +182,7 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
# Allow plugins to modify/wrap the update_handler
update_handler = apply_update_handler_alter(update_handler, watch, datastore)
update_signal = signal('watch_small_status_comment')
update_signal.send(watch_uuid=uuid, status="Fetching page..")
set_watch_minitext_status(watch, "Fetching page..")
# All fetchers are now async, so call directly
await update_handler.call_browser()
@@ -446,6 +468,7 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
# Step 1: AI Change Intent — may suppress notification
_llm_intent, _llm_intent_source = resolve_intent(watch, datastore)
if _llm_intent:
set_watch_minitext_status(watch, "AI/LLM (rules)..")
_llm_result = await loop.run_in_executor(
executor,
lambda diff=_diff_text, snap=contents: evaluate_change(
@@ -465,6 +488,7 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
# Step 2: AI Change Summary — runs for any LLM-configured watch with a change
if changed_detected:
set_watch_minitext_status(watch, "AI/LLM (summary)..")
_change_summary = await loop.run_in_executor(
executor,
lambda diff=_diff_text, snap=contents: summarise_change(
@@ -669,6 +693,8 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
finally:
# Send completion signal - retrieve by name to ensure thread-safe access
if watch:
# Clear transient in-memory status — check is done
watch.pop('__check_status', None)
watch_check_update = signal('watch_check_update')
watch_check_update.send(watch_uuid=watch['uuid'])