fix(activity): runs keep their true outcome and spend, the page reaches every kept row, a closing dialog leaves the server log alone

Backend
- A run's outcome write sits outside the job's own try: a database blip after a run that
  succeeded no longer records it as failed (the row waits for the next start's sweep).
- An interrupted run keeps what it spent (the sweep sums its quota rows, as finish does).
- The Scheduler's ad-hoc row says running while any backfill runs, not only the newest.
- recent_runs reads each job's last runs off its index instead of ranking the whole table.
- An interval change is stored with its audit row in one transaction, then rescheduled.
- One API instance per database is the stated contract (.env.example, jobruns).
- Turning full history on for a channel already complete records no run.
- Migration 0109 round-trips interrupted and ad-hoc runs; the run words' DB CHECKs are
  test-held to the enums; the account filter is one helper for the four activity reads.
- An audit row says how many of its request's log lines are still kept.

Frontend
- An "All" period reaches every row still kept (runs and admin actions outlive 90 days).
- Back/Forward is followed in place by the server log: a closing dialog no longer turns
  Live off; a pivot still opens a fresh log.
- Runs moved over from the audit log read their one-liner in the reader's language.
- Filter choices or accounts that failed to load say so beside the filters, with a retry.
- An old server-log link keeps its account, text and range; a remembered retired page
  opens its tab.
- A request whose lines are gone says so instead of linking to an empty list.
- The log lines and the history rows share one row frame, and the server log one list
  frame with the other tabs; an empty list is announced; each day's quota is readable.
This commit is contained in:
2026-10-18 15:38:32 +02:00
parent 3021be5166
commit fad656988a
44 changed files with 1101 additions and 426 deletions
+50
View File
@@ -7,7 +7,10 @@ with rows that share a timestamp — the case an offset or a timestamp-only curs
from datetime import datetime, timedelta, timezone
from app.auth import admin_user
from app.main import app
from app.models import AuditLog, Channel, JobRun, LogEvent, QuotaEvent
from tests.test_body_limit import _route_objects
T0 = datetime(2026, 10, 1, 12, 0, tzinfo=timezone.utc)
@@ -88,6 +91,32 @@ def test_an_admin_action_keeps_the_request_it_was_made_in(db, make_user, as_user
assert row["request_id"] == r.headers["x-request-id"]
def test_an_admin_action_says_how_many_of_its_requests_lines_are_still_kept(db, make_user, as_user):
"""A request's lines are kept days, its audit row months: a link to lines gone would open an
empty list, so the row says how many are left, as a run does."""
admin = make_user(role="admin")
kept = _audit(db, "config_set", actor=admin.id, request_id="req-kept-0001")
gone = _audit(db, "config_set", actor=admin.id, request_id="req-gone-0001")
db.add_all(
[
LogEvent(
ts=T0,
level="INFO",
sev=20,
source="api",
logger="t",
msg="m",
request_id="req-kept-0001",
)
for _ in range(3)
]
)
db.commit()
items = as_user(admin).get("/api/admin/audit").json()["items"]
lines = {r["id"]: r["log_lines"] for r in items}
assert (lines[kept], lines[gone]) == (3, 0)
def test_the_run_list_filters_pages_and_names_the_channel_and_the_log_lines(db, make_user, as_user):
admin = make_user(role="admin")
db.add(Channel(id="UCchan", title="Aileen Wang"))
@@ -146,6 +175,8 @@ def test_the_quota_breakdown_covers_its_range_and_one_account(db, make_user, as_
def totals(**params) -> dict:
body = client.get("/api/quota/admin", params=params).json()
# The chart is the same filter's: its days add up to the table's total.
assert sum(d["total"] for d in body["daily"]) == sum(r["total"] for r in body["rows"])
return {r["user_id"]: r["total"] for r in body["rows"]}
assert totals() == {admin.id: 150, None: 7} # the last 30 days by default
@@ -153,3 +184,22 @@ def test_the_quota_breakdown_covers_its_range_and_one_account(db, make_user, as_
assert totals(until=(now - timedelta(days=1)).isoformat()) == {admin.id: 50}
assert totals(user="none") == {None: 7}
assert totals(user=str(admin.id)) == {admin.id: 150}
def test_every_admin_read_and_write_is_admin_only():
"""The route table, not a list kept by hand: every /api/admin route and the quota breakdown
resolve `admin_user`, so a dropped dependency cannot hand an account's runs, errors and e-mail
addresses to any signed-in user."""
routes = _route_objects(app)
admin = {k: r for k, r in routes.items() if k[1].startswith(("/api/admin", "/api/quota/admin"))}
assert ("GET", "/api/admin/runs") in admin and ("GET", "/api/quota/admin") in admin
open_ = []
for key, route in admin.items():
stack, calls = [route.dependant], set()
while stack:
d = stack.pop()
calls.add(d.call)
stack.extend(d.dependencies)
if admin_user not in calls:
open_.append(key)
assert open_ == []