# Activity & notifications

Source: https://docs.externa.qiick.io/docs/activity-and-notifications

Externa records domain and auth events with Spatie Activitylog, exposes an Activity Log admin page (and an AI query tool), and delivers file job outcomes through database notifications in the sidebar bell.

<!-- screenshot omitted -->

> **Related**
> Privacy / hosting: [Threat model & hosting](/docs/threat-model). Team chat events: [Chat (hub & items)](/docs/chat). Dashboard widgets that read activity: [Dashboard](/docs/dashboard). File jobs that emit notifications: [File manager](/docs/file-manager). Realtime bell / presence: [Reverb & Echo](/docs/reverb-and-echo). AI logging helpers: [AI assistant model](/docs/ai-assistant).

## Spatie Activitylog

| Item             | Detail                                            |
| ---------------- | ------------------------------------------------- |
| Package model    | `Spatie\Activitylog\Models\Activity`              |
| Config           | `config/activitylog.php`                          |
| Env toggle       | `ACTIVITYLOG_ENABLED`                             |
| Retention        | `clean_after_days` = 365                          |
| Default log name | `default`                                         |
| Cleanup          | `Schedule::command('activitylog:clean')->daily()` |

### LogsApplicationActivity

Trait: `App\Concerns\LogsApplicationActivity` (wraps Spatie `LogsActivity`).

Options: `logAll()`, `logOnlyDirty()`, `dontLogEmptyChanges()`, `useLogName('default')`.

Used on: `User`, `UserGroup`, `Role`, `Collection`, `CollectionField`, `CollectionItem`, `File`, `FileVersion`, `FileUpload`, `ApiKey` (with secret exclusions where applicable).

Item editors surface a compact [activity strip](/docs/collection-items#activity-strip) (last update + revisions/activity links). Field-level history remains in [Item History](/docs/collection-items#item-history-revisions), not Spatie attribute diffs.

### Privacy defaults

Activity properties and chat bodies are **plaintext** in the database (no column encryption). Chat create/delete events store a lean `body_preview` (≤ 120 characters) plus counts — not a full encrypted transcript. Restrict who has `can-show-activity-logs`. Operators should encrypt DB volumes and backups; see [Threat model & hosting](/docs/threat-model).

### Auth login / logout / failed

Registered in `AppServiceProvider::registerAuthActivityListeners()`:

| Event                          | Log name | Activity event | Description / notes                                                  |
| ------------------------------ | -------- | -------------- | -------------------------------------------------------------------- |
| `Illuminate\Auth\Events\Login` | `auth`   | `login`        | `User logged in`; also updates `last_login_at` / `last_login_ip`     |
| `Logout`                       | `auth`   | `logout`       | `User logged out`                                                    |
| `Failed`                       | `auth`   | `failed`       | `Failed login attempt`; `causedByAnonymous()`, property `identifier` |

### AI activity

`App\Ai\Support\AiActivityLogger` writes `log_name=ai` with events `ai_prompt`, `ai_response`, `ai_tool`, `ai_mutation`.

## Activity Log UI

| Item       | Detail                                                  |
| ---------- | ------------------------------------------------------- |
| Controller | `App\Http\Controllers\Admin\ActivityLogController`      |
| Route      | `GET /activity-logs` (`activity-logs.index`)            |
| Middleware | `auth`, `verified`, `permission:can-show-activity-logs` |
| Permission | `can-show-activity-logs`                                |
| Page       | `resources/js/pages/admin/activity-logs/index.tsx`      |

The list is a server-driven data table like the other admin lists — [Users & groups — Admin lists](/docs/users-and-groups#admin-lists-1-4-0).

Filters: search, `user_ids[]` (multi; legacy `user_id` still accepted), `event`, `log_name`, `date_from`, `date_to`, optional `subject_type` + `subject_id` (used by the item form **Activity** link).

### Export CSV / JSON

Same permission and filters as the index. Toolbar **Export CSV** / **Export JSON** hit:

`GET /activity-logs/export?format=csv|json&…filters`

Streamed download (UTF-8 CSV with BOM for Excel). Cap: **10 000** newest matching rows (sync stream — queue/async if you need larger dumps). Columns: `id`, `created_at`, `log_name`, `event`, `description`, `causer_id`, `causer_email`, `subject_type`, `subject_id`, `properties` (JSON string).

Log names include `default`, `auth`, `ai`, `chat`, `settings`.

Filterable events (`App\Support\Activity\FilterableActivityEvents`):

```
created | updated | deleted | restored
login | logout | failed
ai_prompt | ai_response | ai_tool | ai_mutation
settings_updated
chat_message | chat_message_deleted
permissions_synced | roles_synced | groups_synced | members_synced
collection_permissions_synced | file_permissions_synced
```

Expanded rows show Spatie `attribute_changes` when present, otherwise manual `properties.meta` (e.g. chat `body_preview`).

## AI QueryActivityLogs

| Item   | Detail                           |
| ------ | -------------------------------- |
| Tool   | `App\Ai\Tools\QueryActivityLogs` |
| Gate   | `can-show-activity-logs`         |
| Limits | Default 25, max 50               |
| Dates  | `YYYY-MM-DD` only                |
| Events | Same set as the UI filters       |

Registered on `AppAssistant` when the user has `can-show-activity-logs`.

## Notifications (database + bell)

Most use trait `BroadcastsWithDatabase`: `via(['database'])`, plus `broadcast` when `BROADCAST_CONNECTION` is not `log`/`null`.

| Class                                                    | `data.type`                  | Emitted from                   |
| -------------------------------------------------------- | ---------------------------- | ------------------------------ |
| `App\Notifications\FileZipReadyNotification`             | `file_zip_ready`             | `PrepareFilesZipJob`           |
| `App\Notifications\FileZipFailedNotification`            | `file_zip_failed`            | `PrepareFilesZipJob`           |
| `App\Notifications\FileDuplicationCompletedNotification` | `file_duplication_completed` | `DuplicateFilesJob`            |
| `App\Notifications\FileDuplicationFailedNotification`    | `file_duplication_failed`    | `DuplicateFilesJob`            |
| `App\Notifications\ItemChatNotification`                 | `chat`                       | Team chat mentions / notify-me |

Zip-ready notifications point at download route `files.zips.download`. Chat bell rows deep-link into the hub / item thread.

Realtime delivery (Echo private channel `App.Models.User.{id}`) is documented in [Reverb & Echo](/docs/reverb-and-echo). When `BROADCAST_CONNECTION` is `log`/`null`, the sidebar bell keeps its **60s unread poll**; with Reverb enabled there is **no** periodic poll — Echo `.notification()` updates the badge.

## Bell API

| Item       | Detail                                                     |
| ---------- | ---------------------------------------------------------- |
| Controller | `App\Http\Controllers\NotificationController`              |
| Routes     | `routes/web.php`                                           |
| Middleware | `auth` + `verified` only — **no** activity-log permission  |
| UI         | `NotificationsBell` + `NotificationsDrawer` in the sidebar |
| Client     | `resources/js/lib/notifications-api.ts`                    |

| Method | Path | Name | Behavior |
| --- | --- | --- |
| `GET` | `/notifications` | `notifications.index` | Paginate 20 |
| `GET` | `/notifications/unread-count` | `notifications.unread-count` | Unread count |
| `POST` | `/notifications/read` | `notifications.read` | Mark read (`ids` and/or all unread) |
| `POST` | `/notifications/unread` | `notifications.unread` | Mark unread (`ids` required, non-empty) |

### Drawer UX

- Opening the drawer **does not** auto-mark page-1 notifications as read (badge stays until the user acts).
- **Mark all as read** is an explicit header action (`data-test="notifications-mark-all-read"`).
- **Mark as unread** appears on already-read rows (`data-test="notifications-mark-unread"`) and restores the badge count from the API response.

There is no dedicated notification permission in `PermissionEnum`; any authenticated verified user can use the bell API for their own notifications.

## Permissions summary

| Surface                                 | Permission                              |
| --------------------------------------- | --------------------------------------- |
| Activity Log page + `QueryActivityLogs` | `can-show-activity-logs`                |
| Notifications bell / API                | Authenticated + verified (no enum gate) |

## Source map

| Concern             | Location                                                                                                     |
| ------------------- | ------------------------------------------------------------------------------------------------------------ |
| Activity controller | `app/Http/Controllers/Admin/ActivityLogController.php`                                                       |
| Logging trait       | `app/Concerns/LogsApplicationActivity.php`                                                                   |
| Auth listeners      | `AppServiceProvider::registerAuthActivityListeners`                                                          |
| AI query tool       | `app/Ai/Tools/QueryActivityLogs.php`                                                                         |
| Notifications       | `app/Notifications/FileZip*Notification.php`, `FileDuplication*Notification.php`, `ItemChatNotification.php` |
| Bell API            | `app/Http/Controllers/NotificationController.php`                                                            |
