Automation activity log #91

Closed
opened 2026-03-09 00:12:12 +01:00 by myrmidex · 1 comment
Owner

Summary

Surface automation activity so the user can see what FFR has been doing without checking logs.

Context

With automated posting enabled, articles are fetched, validated, approved, and published without user intervention. Currently there's no in-app way to see this activity stream. The ActionPerformed event and LogActionListener exist but only write to the database — nothing surfaces this to the user.

Goals

  • Show a chronological activity feed: articles fetched, validated, approved, published, rejected, errored
  • Filterable by type (fetch/validate/publish/error)
  • Quick glance: "last 24h: 15 fetched, 12 approved, 3 rejected, 1 error"
  • Link to the relevant article from each activity entry

Tasks

  • Activity log UI page (or dashboard widget)
  • Query ActionLog/ActionPerformed records for display
  • Filter controls (by action type, date range)
  • Summary counters (24h/7d)
  • Tests for activity log queries
## Summary Surface automation activity so the user can see what FFR has been doing without checking logs. ## Context With automated posting enabled, articles are fetched, validated, approved, and published without user intervention. Currently there's no in-app way to see this activity stream. The ActionPerformed event and LogActionListener exist but only write to the database — nothing surfaces this to the user. ## Goals - Show a chronological activity feed: articles fetched, validated, approved, published, rejected, errored - Filterable by type (fetch/validate/publish/error) - Quick glance: "last 24h: 15 fetched, 12 approved, 3 rejected, 1 error" - Link to the relevant article from each activity entry ## Tasks - [ ] Activity log UI page (or dashboard widget) - [ ] Query ActionLog/ActionPerformed records for display - [ ] Filter controls (by action type, date range) - [ ] Summary counters (24h/7d) - [ ] Tests for activity log queries
myrmidex added this to the v1.3.0 milestone 2026-03-09 00:12:12 +01:00
myrmidex added the
enhancement
label 2026-03-09 00:24:27 +01:00
myrmidex modified the milestone from v1.3.0 to v1.4.0 2026-03-18 18:12:14 +01:00
myrmidex self-assigned this 2026-08-10 19:56:19 +02:00
Author
Owner

Implemented on release/v1.4.0 in six commits.

Deviation from the ticket

The ticket referenced an ActionLog model that does not exist, and assumed the logs table could be filtered by action type — it cannot, it only has level and a free-text message. logs is also a general diagnostic log containing PHP TypeErrors and migration failures, so it is not a usable activity feed.

A dedicated activity_logs table was added instead, with a polymorphic subject and its own ActivityLogged event / RecordActivityListener. The existing ActionPerformed → logs pipeline is untouched.

Not delivered as specified

"Link to the relevant article from each activity entry" is not a link. The Livewire Articles component has no #[Url]/query-string binding, so a deep link would be silently ignored. Entries instead show the article's feed name and, where present, the failure reason. Making this a real link needs query-string support on Articles — worth a follow-up ticket if wanted.

Delivered

  • ActivityTypeEnum — fetch / validate / approve / reject / publish / error
  • activity_logs table, model, factory; scopes ofType / since / latestFirst
  • Dispatch sites: feed discovery, validation, publish (success/skip/failure), approve, reject
  • ActivitySummary — per-type counts for a window in one grouped query
  • Livewire Activity page: paginated feed, type filter, date-range filter, 24h/7d counters
  • CleanupActivityLogsJob — 90-day retention, daily (nothing prunes logs)

Bugs found and fixed along the way

Two duplicate-dispatch bugs, both pre-existing and unrelated to this feature:

  1. Four listeners were registered twice — explicitly in AppServiceProvider::boot() and by Laravel 12 auto-discovery of app/Listeners. Every logs row was written twice, every article validated twice, and every approval ran PublishApprovedArticleListener twice — duplicate-publish pressure that #123's Cache::lock was absorbing.
  2. ExceptionOccurred was dispatched twice — identical reportable() callbacks in both bootstrap/app.php and AppServiceProvider::boot().

Both are pinned by EventListenerRegistrationTest. AppServiceProvider::boot() is now empty; exception reporting lives in bootstrap/app.php only.

Other behaviour changes

  • RouteArticle::reject() gained an isRejected() early-return guard, mirroring the existing one on approve().
  • An empty fetch now records an error activity rather than "Fetched 0 articles" — per #116 an empty result means unparseable, not "no news".
  • Bulk Articles::clear() records one summary entry, not one per article.

Verification

948 tests / 2502 assertions passing (909 before this ticket), Pint clean (303 files), PHPStan clean. Page rendered in a browser.

Implemented on `release/v1.4.0` in six commits. ## Deviation from the ticket The ticket referenced an **`ActionLog` model that does not exist**, and assumed the `logs` table could be filtered by action type — it cannot, it only has `level` and a free-text `message`. `logs` is also a general diagnostic log containing PHP TypeErrors and migration failures, so it is not a usable activity feed. A dedicated **`activity_logs`** table was added instead, with a polymorphic `subject` and its own `ActivityLogged` event / `RecordActivityListener`. The existing `ActionPerformed` → `logs` pipeline is untouched. ## Not delivered as specified **"Link to the relevant article from each activity entry"** is *not* a link. The Livewire `Articles` component has no `#[Url]`/query-string binding, so a deep link would be silently ignored. Entries instead show the article's feed name and, where present, the failure reason. Making this a real link needs query-string support on `Articles` — worth a follow-up ticket if wanted. ## Delivered - `ActivityTypeEnum` — fetch / validate / approve / reject / publish / error - `activity_logs` table, model, factory; scopes `ofType` / `since` / `latestFirst` - Dispatch sites: feed discovery, validation, publish (success/skip/failure), approve, reject - `ActivitySummary` — per-type counts for a window in one grouped query - Livewire `Activity` page: paginated feed, type filter, date-range filter, 24h/7d counters - `CleanupActivityLogsJob` — 90-day retention, daily (nothing prunes `logs`) ## Bugs found and fixed along the way Two duplicate-dispatch bugs, both pre-existing and unrelated to this feature: 1. **Four listeners were registered twice** — explicitly in `AppServiceProvider::boot()` *and* by Laravel 12 auto-discovery of `app/Listeners`. Every `logs` row was written twice, every article validated twice, and **every approval ran `PublishApprovedArticleListener` twice** — duplicate-publish pressure that #123's `Cache::lock` was absorbing. 2. **`ExceptionOccurred` was dispatched twice** — identical `reportable()` callbacks in both `bootstrap/app.php` and `AppServiceProvider::boot()`. Both are pinned by `EventListenerRegistrationTest`. `AppServiceProvider::boot()` is now empty; exception reporting lives in `bootstrap/app.php` only. ## Other behaviour changes - `RouteArticle::reject()` gained an `isRejected()` early-return guard, mirroring the existing one on `approve()`. - An empty fetch now records an **error** activity rather than "Fetched 0 articles" — per #116 an empty result means unparseable, not "no news". - Bulk `Articles::clear()` records one summary entry, not one per article. ## Verification 948 tests / 2502 assertions passing (909 before this ticket), Pint clean (303 files), PHPStan clean. Page rendered in a browser.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/fedi-feed-router#91
No description provided.