Activity log
View on GitHubActivity log
Every project-changing action across the app writes a plain-text
ActivityLog row — "Changed Jane's role to admin", "Deleted the
"Owner" role" — and it's read back in two places today: the
Dashboard's "Recent Work Activity" panel (per-user, via
ActivityLogService::paginateForUser()) and a project's Activity
issue view (per-project, via paginateForProject()), both paginated. This category
documents the write side (already used everywhere) and how to surface
the read side in a third place.
Guides, in the order you'd actually need them
- Log a new kind of activityActivity logLog a new kind of activityThere's no enum, no registration step, no frontend map to update — logging a new kind of activity is a single method call, wherever the action already happ… — the one-line pattern every Service already follows; read this first since it's the pattern you're almost certainly already using correctly without realizing it's "the activity log system."
- Surface the activity log in the UIActivity logSurface the activity log in the UIActivityLogService::paginateForUser() already powers the Dashboard's "Recent Work Activity" panel, and paginateForProject() already powers a project's Acti…
— worked example adding a third read surface (an issue's own
activity), reusing the exact
ActivityLogs/ActivityLogItemcomponents the Dashboard and project Activity view already share.
The architecture in one paragraph
ActivityLog (app/Models/ActivityLog.php) is deliberately unstructured:
project_id (nullable — null for account-level activity like a
password change), user_id, and a single free-text body column —
there is no type/action enum to extend, unlike
notificationsNotifications (in-app & email)Orbit tells users about activity two ways: an in-app notification (the bell dropdown) and an email, per App\Enums\Notifications\NotificationType — per-user… or
alertsUI alerts (toasts)The small, transient toast in the top-right corner of the screen — a one-line confirmation or error, auto-dismissing after a few seconds — is a completely…. ActivityLogService::log(?int $projectId, string $body, ?int $userId = null) is the one method every other
Service calls as a side effect of whatever it's actually doing —
$userId defaults to auth()->id() when omitted, which is why most
call sites (see e.g. RoleService/ProjectMemberService) never pass
it explicitly. On the read side, ActivityLogRepository has
paginateForUser() and paginateForProject() (newest first with the
id as a tie-breaker, eager-loading user, page size clamped to
ActivityLogRepository::MAX_PER_PAGE), plus the unpaginated
getRecentForUser() and getRecentForProject() (latest()->limit($limit))
for surfaces that only need a short list. DashboardController::index()
maps paginateForUser($userId, 10) into an activityLogs prop, and
ProjectController::show() maps paginateForProject($project->id, 20)
into its own activityLogs prop for the Activity issue view (see
../issue-views/README.mdIssue views (List / Board / Calendar / Activity)A project's issues render as a sortable table, a status-grouped Kanban board, a calendar laid out by issue dates, or a chronological activity feed — the sa…). Those props are
Laravel paginators ({ data, links, from, to, total, ... }, mapped with
->through()), and the pager reads its page and size from
activityPage/activityPerPage
(ActivityLogService::PAGE_NAME/PER_PAGE_NAME) so it doesn't collide
with the issue list's page/perPage on the project page. On the
frontend both pages render the shared Pagination molecule under the
list, passing pageParam, perPageParam and defaultPerPage
(ACTIVITY_PAGE_PARAM/ACTIVITY_PER_PAGE_PARAM in
resources/js/utils/activityLog.ts). Both Controllers map to the exact
same entry shape —
{ id, body, userName, createdAt }, createdAt already
human-formatted server-side via created_at->diffForHumans() — typed
once as ActivityLogEntry (resources/js/types/ActivityLog.ts) and
rendered by the same pair of frontend components regardless of which
page it's on: ActivityLogs (list + empty state) wrapping
ActivityLogItem (one entry — a colored dot derived from the body
text via getActivityLogVisual() in resources/js/utils/activityLog.ts,
the user name, the body, and the timestamp). Reusing that exact pair
of components is what makes guide 2's third surface a small change
rather than a new UI from scratch.
