Add two-stage run verification and 24-hour Activity filter
All checks were successful
Deploy / deploy (push) Successful in 57s
All checks were successful
Deploy / deploy (push) Successful in 57s
Status now reads "Sent" when the confirmation click lands, since that only means MassPrice accepted the run request, not that it finished — their own processing is async and takes anywhere from ~1 to ~22 minutes in observed practice. A new "Complete" column tracks that separately: run_rule() captures a watermark (the target rule's current top data-activitylogid on /Activity/List, not the displayed timestamp string, to sidestep fragile date/timezone parsing) before submitting, and scheduler.py schedules a one-off verify_run() job VERIFY_DELAY_MINUTES later to check whether a new "Complete" entry appeared past that watermark. rebuild_jobs() now only clears recurring "rule-*" jobs, not the one-off "verify-*" jobs, so editing a schedule can't cancel a verification that's already in flight for an earlier run. Also scopes the Activity page to the last 24 hours per request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H6TgffxTvYhqxykAbVQg2v
This commit is contained in:
parent
988db5d5c4
commit
8b58dadc5a
8 changed files with 249 additions and 20 deletions
130
automation.py
130
automation.py
|
|
@ -32,6 +32,7 @@ authenticates before accepting it.
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
|
@ -163,11 +164,36 @@ def list_rules():
|
|||
browser.close()
|
||||
|
||||
|
||||
def _find_latest_activity(page, rule_name):
|
||||
"""Returns (activity_id: int, status_text: str) for the most recent
|
||||
Activity Log entry matching rule_name, or (None, None) if there isn't
|
||||
one. Each Activity Log row carries a stable, monotonically increasing
|
||||
`data-activitylogid` — comparing that (not the displayed timestamp
|
||||
string, which would need fragile date/timezone parsing) is how a
|
||||
"new completion appeared" check is done reliably."""
|
||||
page.goto(f"{BASE_URL}/Activity/List")
|
||||
page.wait_for_timeout(1000)
|
||||
rows = page.locator("div.activity[data-activitylogid]").filter(
|
||||
has=page.locator(f'.activity__name[title="{rule_name}"]')
|
||||
)
|
||||
if rows.count() == 0:
|
||||
return None, None
|
||||
row = rows.first
|
||||
activity_id = int(row.get_attribute("data-activitylogid"))
|
||||
status_text = row.locator(".activity__runtime").first.inner_text().strip()
|
||||
return activity_id, status_text
|
||||
|
||||
|
||||
def run_rule(rule_id, rule_name=None):
|
||||
"""Click "Run Rule" for the given rule id. Logs the outcome to
|
||||
run_history.json and returns (status, message)."""
|
||||
"""Click "Run Rule" (through its confirmation modal) for the given
|
||||
rule id. Logs the outcome to run_history.json and returns a dict:
|
||||
{"status": "sent"|"failure", "message": ..., "entry_id": ...,
|
||||
"watermark_activity_id": ...}. On "sent", the caller (scheduler.py)
|
||||
schedules a delayed verify_run() call using entry_id/watermark_activity_id
|
||||
to confirm the run actually completed on MassPrice's side."""
|
||||
status = "failure"
|
||||
message = ""
|
||||
watermark_id = None
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, args=LAUNCH_ARGS)
|
||||
try:
|
||||
|
|
@ -176,6 +202,20 @@ def run_rule(rule_id, rule_name=None):
|
|||
_ensure_authenticated(page)
|
||||
_save_state(context)
|
||||
|
||||
# Capture a watermark before submitting, so the later
|
||||
# verification check can tell "a new completion appeared"
|
||||
# apart from "the same old entry is still sitting there".
|
||||
# Best-effort: if this fails for any reason, proceed with the
|
||||
# actual run anyway — the run itself matters more than the
|
||||
# optional follow-up verification.
|
||||
if rule_name:
|
||||
try:
|
||||
watermark_id, _ = _find_latest_activity(page, rule_name)
|
||||
except Exception:
|
||||
logger.exception("Failed to capture activity watermark for %s", rule_name)
|
||||
page.goto(f"{BASE_URL}/Rule/List")
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
run_selector = f'a.run-rule[data-rule-id="{rule_id}"]'
|
||||
row = page.locator("li.rule.widget-rule").filter(
|
||||
has=page.locator(run_selector)
|
||||
|
|
@ -206,31 +246,109 @@ def run_rule(rule_id, rule_name=None):
|
|||
modal.wait_for(state="hidden", timeout=10000)
|
||||
page.wait_for_timeout(1000) # let the confirmed run's request land
|
||||
|
||||
status = "success"
|
||||
message = "Run Rule clicked"
|
||||
status = "sent"
|
||||
message = "Run Rule confirmed"
|
||||
except Exception as exc:
|
||||
message = str(exc)
|
||||
logger.exception("run_rule failed for rule %s", rule_id)
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
_log_history(rule_id, rule_name, status, message)
|
||||
return status, message
|
||||
entry_id = _log_history(rule_id, rule_name, status, message)
|
||||
will_verify = status == "sent" and bool(rule_name)
|
||||
if will_verify:
|
||||
_set_verification_pending(entry_id)
|
||||
|
||||
return {
|
||||
"status": status,
|
||||
"message": message,
|
||||
"entry_id": entry_id,
|
||||
"watermark_activity_id": watermark_id,
|
||||
"will_verify": will_verify,
|
||||
}
|
||||
|
||||
|
||||
def verify_run(entry_id, rule_name, watermark_id):
|
||||
"""Delayed follow-up: check whether the run logged as entry_id
|
||||
actually completed on MassPrice's side by now. Updates that
|
||||
run_history.json entry's "verification" field in place."""
|
||||
verification = "not_confirmed"
|
||||
verify_message = ""
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, args=LAUNCH_ARGS)
|
||||
try:
|
||||
context = _new_context(browser)
|
||||
page = context.new_page()
|
||||
_ensure_authenticated(page)
|
||||
_save_state(context)
|
||||
|
||||
activity_id, status_text = _find_latest_activity(page, rule_name)
|
||||
if activity_id is None:
|
||||
verify_message = "No matching activity found on MassPrice"
|
||||
elif watermark_id is not None and activity_id <= watermark_id:
|
||||
verify_message = "No new completion appeared within the allotted time"
|
||||
elif status_text and "complete" in status_text.lower():
|
||||
verification = "complete"
|
||||
else:
|
||||
verify_message = f"Latest activity shows \"{status_text}\", not Complete"
|
||||
except Exception as exc:
|
||||
verify_message = str(exc)
|
||||
logger.exception("verify_run failed for entry %s", entry_id)
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
_update_verification(entry_id, verification, verify_message)
|
||||
|
||||
|
||||
def _log_history(rule_id, rule_name, status, message):
|
||||
entry_id = uuid.uuid4().hex
|
||||
history = storage.load("run_history.json", [])
|
||||
history.insert(
|
||||
0,
|
||||
{
|
||||
"id": entry_id,
|
||||
"rule_id": rule_id,
|
||||
"rule_name": rule_name,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"status": status,
|
||||
"message": message,
|
||||
# "pending" only when a verify_run() has actually been
|
||||
# scheduled for this entry (status == "sent" and rule_name
|
||||
# was known) — set explicitly by run_rule() below, not here,
|
||||
# since _log_history() itself doesn't know whether scheduling
|
||||
# succeeded.
|
||||
"verification": None,
|
||||
"verify_message": None,
|
||||
},
|
||||
)
|
||||
storage.save("run_history.json", history[:200])
|
||||
return entry_id
|
||||
|
||||
|
||||
def _set_verification_pending(entry_id):
|
||||
history = storage.load("run_history.json", [])
|
||||
for entry in history:
|
||||
if entry.get("id") == entry_id:
|
||||
entry["verification"] = "pending"
|
||||
break
|
||||
else:
|
||||
return
|
||||
storage.save("run_history.json", history)
|
||||
|
||||
|
||||
def _update_verification(entry_id, verification, verify_message):
|
||||
history = storage.load("run_history.json", [])
|
||||
for entry in history:
|
||||
if entry.get("id") == entry_id:
|
||||
entry["verification"] = verification
|
||||
entry["verify_message"] = verify_message
|
||||
entry["verified_at"] = datetime.now(timezone.utc).isoformat()
|
||||
break
|
||||
else:
|
||||
# Entry aged out of the 200-cap between being logged and this
|
||||
# delayed check running — nothing to update, not an error.
|
||||
return
|
||||
storage.save("run_history.json", history)
|
||||
|
||||
|
||||
# ── Login-session import (paste cookies exported from a real browser) ───
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue