Версія v1.6.5 Попередні версії: документація на GitHub
Сповіщення та фонові дії
На цій сторінці
- Швидкий старт
- Дзвіночок
- Як оновлюється
- Надсилання сповіщень
- Власний клас
- Фонові дії
- Як виконується
- Налаштування черги
- Обмеження handle() у черзі
- Ліміти
- Скасування
- Завислі запуски
- Результат
- ActionResponse::file()
- Драйвер sync
- Журнал аудиту
- Подія ActionRunFinished
- Realtime
- Приклад: Reverb
- CSP
- Конфігурація
- Міграції
- Очищення
- Безпека
- HTTP-ендпоінти
- Продуктивність
- Обмеження
У шапці панелі з'являється дзвіночок зі сповіщеннями користувача. Сповіщення надсилає застосунок
(класом LarsadminNotification) або сама панель, коли завершується фонова дія. Дія з $queued
виконується в черзі Laravel: користувач одразу отримує підтвердження, бачить прогрес у дзвіночку, може
скасувати запуск, а після завершення отримує сповіщення з результатом.
Оновлення приходять опитуванням. Якщо в застосунку налаштовано Reverb або Pusher, панель отримує сигнали в реальному часі (див. Realtime).
Можливість вимкнено за замовчуванням. Поки її вимкнено, панель не робить жодних додаткових запитів,
нові маршрути відповідають 404, а дії з $queued виконуються синхронно, як звичайні. Додаткових
залежностей Composer чи npm не потрібно: клієнт laravel-echo і pusher-js уже вбудовано в зібрані
асети панелі.
Швидкий старт
-
Створіть таблицю
notificationsLaravel (якщо її ще немає) і опублікуйте міграції панелі (larsadmin:installу нових проєктах уже публікує їх):php artisan make:notifications-table php artisan vendor:publish --tag=larsadmin-migrations php artisan migrate -
Модель користувача панелі має використовувати трейт
Notifiable(стандартнийApp\Models\Userуже його має). -
Увімкніть можливість у
.env:LARSADMIN_NOTIFICATIONS=true QUEUE_CONNECTION=database -
Запустіть обробник черги (у продакшні через Supervisor чи аналог).
retry_afterз'єднання черги має бути більшим за--timeoutобробника й$timeoutкожної фонової дії (див. Налаштування черги):php artisan queue:work --timeout=600 -
Заплануйте очищення в
routes/console.php:use Illuminate\Support\Facades\Schedule; Schedule::command('larsadmin:prune-notifications')->daily();
Тепер дзвіночок показується в шапці, а дії можна позначати як фонові.
Увага. Якщо можливість увімкнено, а таблиць немає, сторінки панелі працюють, але дзвіночок показується без лічильника, а помилка передається в
report(). Запити дзвіночка й запуск фонової дії завершуються винятком з підказкою «run the larsadmin migrations ... make:notifications-table». Виконайте міграції або повернітьLARSADMIN_NOTIFICATIONS=false.
Дзвіночок
Дзвіночок стоїть у шапці між пошуком та ім'ям користувача. Він показує лише сповіщення панелі (тип
larsadmin у таблиці notifications), адресовані поточному користувачу guard-а панелі. Інші сповіщення
застосунку в ньому не з'являються.
- Лічильник непрочитаних показується числом (понад 99 як «99+»); при нулі лічильника немає. Поки виконується фонова дія користувача, на дзвіночку видно індикатор активності.
- Список відкривається кліком як випадне вікно: найновіші першими, по 15 (
per_page), «Показати ще» дописує наступні. Кожне сповіщення має рівень (інформація, успіх, попередження, помилка), заголовок, текст, час і, якщо задано, посилання або кнопку завантаження. - Клік по сповіщенню позначає його прочитаним і відкриває посилання. Кожне сповіщення можна позначити прочитаним або видалити окремою кнопкою; «Позначити всі прочитаними» позначає весь список.
- «Фонові задачі» над списком показують активні запуски й завершені за останні 24 години (до 10): статус, смугу прогресу «400 з 1200», кнопку «Скасувати» і посилання на результат.
- Нові сповіщення з'являються також як спливні повідомлення (до трьох одночасно, решта «Ще N сповіщень»).
Якщо сесія користувача закінчилася і фоновий запит дзвіночка отримав 401 або 419, сторінка не перезавантажується: опитування й realtime зупиняються, а спливне повідомлення «Сеанс завершився. Оновіть сторінку, щоб увійти знову.» лишається, доки користувач його не закриє. Позначення прочитаним, видалення й скасування не відкочуються відповіддю списку, що була в дорозі під час дії.
Esc, клік поза вікном або Tab за його межі закривають список і повертають фокус на дзвіночок. На вузьких
екранах список займає всю ширину під шапкою. Дзвіночок доступний з клавіатури та для скрінрідерів
(WCAG 2.1 AA): назва кнопки містить кількість непрочитаних («Сповіщення, 3 непрочитані»), непрочитане
позначається не лише кольором, прогрес передається як progressbar.
Дзвіночка немає, якщо можливість вимкнено, користувач не має доступу до панелі або його модель не
використовує Notifiable.
Як оновлюється
| Що | Без realtime | З realtime |
|---|---|---|
| Лічильник і нові сповіщення | GET api/notifications/status кожні poll_interval с (60), лише коли вкладка видима, і одразу після повернення на вкладку; також при кожному переході в панелі |
за сигналом; опитування status лишається як запасне |
| Активні запуски | кожні 3 с, через 2 хвилини кожні 10 с | за сигналами і запасне опитування кожні 30 с |
Після помилки запиту інтервал опитування подвоюється (до 5 хвилин). poll_interval = 0 вимикає
опитування: лічильник оновлюється лише при переходах між сторінками.
Надсилання сповіщень
Maxcxam\Larsadmin\Notifications\LarsadminNotification це звичайне сповіщення Laravel, яке зберігається
каналом database з типом larsadmin:
use Maxcxam\Larsadmin\Notifications\LarsadminNotification;
$user->notify(
LarsadminNotification::make('Імпорт завершено')
->body('Додано 120 товарів.')
->success()
->url(route('larsadmin.entities.index', 'products', false), 'Переглянути'),
);
LarsadminNotification::make('Планове оновлення о 22:00')
->warning()
->sendTo(User::where('is_admin', true)->get());
| Метод | Опис |
|---|---|
make(string $title) |
новий екземпляр; заголовок обрізається з країв, до 200 символів, порожній дає InvalidArgumentException |
title(string $title) |
змінити заголовок (ті самі правила) |
body(?string $body) |
текст під заголовком, до 2000 символів; простий текст, переноси рядків зберігаються |
level(string $level) |
info (за замовчуванням), success, warning або danger; інше дає InvalidArgumentException |
info(), success(), warning(), danger() |
скорочення для level() |
icon(?string $icon) |
bell, check, alert, info, download, file, history або null (іконка рівня); інше дає InvalidArgumentException |
url(?string $url, ?string $label = null) |
посилання зі сповіщення і його підпис (до 200 символів) |
sendTo(mixed $notifiables) |
те саме, що Notification::send($notifiables, $this) |
Сповіщення отримують лише моделі з трейтом Notifiable. Дзвіночок показує сповіщення моделі
користувача guard-а панелі: сповіщення іншій моделі (іншому guard-у) у ньому не з'являться.
Посилання. url() приймає лише URL того самого застосунку: відносний шлях, що починається з одного
/ (/admin/..., не //host), або абсолютний http(s) URL на хості поточного запиту, хості
APP_URL чи домені панелі (larsadmin.domain). Інакше кидається InvalidArgumentException. Під час
читання посилання перевіряється ще раз, а браузер відкриває лише посилання на тому самому origin.
Сповіщення з недійсним посиланням показується без нього. Шляхи панелі відкриваються без
перезавантаження сторінки, інші шляхи застосунку повним переходом.
Текст. Заголовок і текст зберігаються як передано й показуються як простий текст (HTML не
інтерпретується). Перекладайте їх самі, наприклад у мові отримувача через HasLocalePreference або
->locale(), як для будь-якого сповіщення Laravel.
Черга. Клас не реалізує ShouldQueue: запис у таблицю це один INSERT. Щоб надсилати через чергу,
додайте ShouldQueue у підкласі.
Власний клас
LarsadminNotification не final. Підклас може, наприклад, додати лист:
use Illuminate\Notifications\Messages\MailMessage;
use Maxcxam\Larsadmin\Notifications\LarsadminNotification;
class ImportFinished extends LarsadminNotification
{
public function via(object $notifiable): array
{
return ['database', 'mail'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)->line('Імпорт завершено.');
}
}
Не змінюйте databaseType() і toArray(): за ними дзвіночок знаходить і показує сповіщення. Клас, що
не спадкує LarsadminNotification, теж потрапить у дзвіночок, якщо його databaseType() повертає
'larsadmin', а toArray() дає таку форму:
{ "v": 1, "title": "Імпорт завершено", "body": "Додано 120 товарів.", "level": "success",
"icon": "check", "url": "/admin/entities/products", "urlLabel": "Переглянути", "run": null }
Обов'язковий лише title. Невідомі level і icon замінюються на info і іконку рівня, а url
перевіряється за правилами вище.
Фонові дії
Дія з $queued виконується в черзі. Модальне вікно дії заздалегідь попереджає, що вона виконається у
фоні. Після запуску користувач одразу бачить повідомлення на зразок «Експорт CSV» виконується у фоні.
Прогрес — у сповіщеннях. Сам прогрес показується в розділі «Фонові задачі» дзвіночка.
use App\Models\Post;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Maxcxam\Larsadmin\Actions\Action;
use Maxcxam\Larsadmin\Actions\ActionFields;
use Maxcxam\Larsadmin\Actions\ActionResponse;
class ExportPostsCsv extends Action
{
public bool $queued = true;
public ?string $queue = 'exports';
public ?int $timeout = 600;
public bool $standalone = true;
public function handle(ActionFields $fields, Collection $models): ?ActionResponse
{
// Унікальна назва: запуски різних користувачів ніколи не ділять один файл.
$path = 'exports/posts-'.Str::ulid().'.csv';
Storage::disk('local')->put($path, Post::query()->pluck('title')->implode("\n"));
return ActionResponse::file('local', $path, 'posts.csv');
}
}
Те саме в ланцюжку, без зміни класу:
public function actions(Request $request): array
{
return [
RecalculateViews::make()->queued()->onQueue('heavy')->onQueueConnection('redis'),
];
}
| Властивість / метод | За замовчуванням | Опис |
|---|---|---|
$queued / queued(bool $queued = true) |
false |
виконувати в черзі. Діє лише з увімкненими сповіщеннями, інакше дія виконується синхронно |
$queue / onQueue(?string $queue) |
null |
назва черги; null бере actions.queue (LARSADMIN_QUEUE), потім чергу з'єднання за замовчуванням |
$queueConnection / onQueueConnection(?string $connection) |
null |
з'єднання черги; null бере actions.queue_connection (LARSADMIN_QUEUE_CONNECTION), потім queue.default |
$timeout |
null |
таймаут job-а в секундах; null бере --timeout обробника |
runsQueued() |
чи піде запуск у чергу: $queued і можливість увімкнено |
Як виконується
- Постановка. Панель перевіряє те саме, що й для синхронної дії (доступ,
viewAny,canSee, валідацію полів), рахує вибрані записи і створює запуск у таблиціlarsadmin_action_runs. У чергу йде один jobRunQueuedAction, який несе лише ідентифікатор запуску. - Виконання. Обробник атомарно «захоплює» запуск (
queued → running), тож повторна доставка того самого job-а нічого не робить. Записи обробляються пакетами поchunkSize, кожен пакет у власній транзакції, як у синхронній дії. Після коміту кожного пакета оновлюється прогрес. - Завершення. Запуск отримує кінцевий статус (
completed,failed,cancelled), власник отримує сповіщення, а застосунок подіюActionRunFinished.
Один job на запуск, tries = 1, без автоматичних повторів: handle() не обов'язково ідемпотентний.
Якщо обробник упав або перевищив таймаут, запуск завершується як failed, а вже оброблені пакети
лишаються збереженими («Виконано для :done з :total, далі помилка.»). Текст винятку користувач не бачить,
виняток передається в report(). Кожен запуск надсилає сповіщення рівно один раз, навіть якщо job
доставлено повторно або запуск водночас визнано завислим.
Від імені користувача. Обробник діє як користувач, що запустив дію: знаходить його через провайдер
guard-а панелі, відтворює запит до шляху сторінки, з якої дію запущено, і на час job-а підміняє
request(), сесію, Auth і мову, а потім відновлює їх. Хост відтвореного запиту береться з app.url
(або larsadmin.domain), а не із заголовка Host оригінального запиту. Усі перевірки повторюються в
обробнику (fail closed):
- доступ до панелі (
viewLarsadmin); - Entity досі зареєстрована, користувач має
viewAny; - дія досі є в
actions()і проходитьcanSee(); - для
standalone-діїauthorizedToRunStandalone(), інакшеauthorizedToRun()для кожного запису; - записи беруться з
indexQuery()(для «усіх знайдених») абоdetailQuery()(для вибраних).
Якщо користувача видалено, права відкликано, дію чи Entity прибрано, запуск завершується «Немає прав на
виконання цієї дії.». Записи, що не пройшли authorizedToRun(), пропускаються, як у синхронній дії.
Перед кожним наступним пакетом обробник перечитує користувача й знову перевіряє доступ до панелі: права,
відкликані посеред довгого запуску, зупиняють його після поточного пакета.
Вибір «усі знайдені» не зберігає список ключів: обробник заново будує запит з фільтрів і пошуку,
тож нові чи змінені записи знову проходять область і авторизацію. Кількість уточнюється на старті, тому
підсумок може відрізнятися від побаченого при запуску. Ліміт queued_max_all перевіряється ще раз: якщо
тепер збігається більше записів, запуск завершується помилкою «Вибрано забагато записів…».
Що зберігається. Вибір, фільтри, пошук, провалідовані значення полів, URL сторінки й дані користувача
для журналу аудиту зберігаються в одному полі payload, зашифрованому APP_KEY. Після завершення
запуску (будь-яким статусом) payload стирається. Зміна APP_KEY між постановкою і виконанням (без
APP_PREVIOUS_KEYS) завершує запуск помилкою.
Налаштування черги
retry_after з'єднання черги (config/queue.php) має бути більшим за --timeout обробника і за
$timeout кожної фонової дії. Інакше черга видасть job повторно, поки він ще виконується. Повторна
доставка нічого не зробить, але черга триматиме job двічі. Якщо $timeout дії не менший за
retry_after, постановка пише попередження в лог.
// config/queue.php
'connections' => [
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 900, // більше за --timeout=600 і за $timeout кожної фонової дії
],
],
php artisan queue:work --timeout=600
Обмеження handle() у черзі
- Сесії немає:
session(), фасадSessionі$request->session()уhandle()(а також у політиках і скоупах) повертають окрему порожню сесію в пам'яті, створену для цього запуску. Вона не ділиться з іншими запусками в тому самому обробнику й зникає після job-а. Відтворенийrequest()не має cookies, файлів, заголовків оригінального запиту й маршруту ($request->route()дорівнюєnull). ЯкщоcanSee(),canRun()чиfields()залежать від сесії чи заголовків, у черзі вони отримають порожні значення. - Значення полів зберігаються так, як переживають JSON: дати приходять рядком ISO-8601
(
Carbon::parse($fields->published_at)), enum як значення (valueабоname),NANтаINFякnull. Значення нормалізуються до постановки в чергу: інший об'єкт у значенні поля (зокрема зactionValue()поля розширення) кидаєLogicExceptionз ім'ям поля, до створення запису запуску. - Поля
FileіImageта поля зmultipart: trueу діях не підтримуються (див. Дії).
Ліміти
| Що | Ключ | За замовчуванням | Що бачить користувач |
|---|---|---|---|
Активних запусків (queued + running) на користувача |
actions.queued_max_active |
3 |
«Забагато фонових завдань. Дочекайтеся завершення попередніх (максимум 3).» |
| Записів для «усіх знайдених» | actions.queued_max_all |
10000 |
422, як для синхронної дії |
actions.max_all (1000) на фонові дії не діє. Список вибраних ідентифікаторів обмежено так само, як для
синхронної дії.
Ліміт активних запусків атомарний: підрахунок і створення запуску виконуються під блокуванням кешу для
кожного користувача, тож паралельні запити не обійдуть його. Потрібне сховище кешу з підтримкою
блокувань (database, redis, file, memcached тощо); без нього підрахунок виконується без
блокування. Завислі запуски до ліміту не враховуються.
Скасування
Кнопка «Скасувати» в «Фонових задачах»:
- запуск у черзі скасовується одразу: обробник його вже не виконає;
- запуск, що виконується, зупиняється після поточного пакета. Пакет завершується повністю (його транзакція не переривається), далі обробка припиняється.
Скасований запуск має статус cancelled і повідомлення «Оброблено :done з :total записів.». Якщо
запуск уже завершився, сервер відповідає 409. Завислий запуск скасовується одразу. Запуск, який понад
хвилину лежить у черзі, позначається «Очікує на обробник черги»: перевірте, чи працює queue:work.
Завислі запуски
Якщо обробник зник (процес убито, job загублено), запуск не висить активним вічно:
| Статус | Коли вважається завислим |
|---|---|
running |
немає «серцебиття» (updated_at, оновлюється після кожного пакета) довше, ніж $timeout дії плюс 60 с; без $timeout довше, ніж actions.stale_after (3600 с) плюс 60 с |
queued |
лежить у черзі довше, ніж actions.queued_stale_after (86 400 с) |
Завислий запуск завершується один раз зі статусом failed («Під час виконання сталася помилка.» або
«Виконано для :done з :total, далі помилка.»), власник отримує сповіщення, а payload стирається.
Перевірка відбувається під час підрахунку ліміту активних запусків, опитування status, у списку й
перегляді запусків і в larsadmin:prune-notifications. Якщо обробник ще живий і запише прогрес, запуск
завершиться без нього: наступний пакет уже не почнеться.
Результат
handle() повернув |
Статус і рівень | Що отримує користувач |
|---|---|---|
null або message() |
completed, успіх |
повідомлення дії або «Дію виконано.», з «N записів пропущено (немає дозволу)» |
danger() |
completed, помилка |
повідомлення дії |
redirect($url) |
completed, успіх |
посилання «Відкрити» в сповіщенні |
download($url, $filename) |
completed, успіх |
посилання «Завантажити posts.csv» |
file($disk, $path, $filename) |
completed, успіх |
завантаження файлу, лише власнику запуску |
| жоден запис не знайдено | completed, попередження |
«Жодного запису не знайдено.» |
| жоден запис не пройшов авторизацію | failed |
«Немає прав на виконання цієї дії.» |
| виняток | failed |
«Під час виконання сталася помилка.» або «Виконано для :done з :total, далі помилка.» |
| скасовано | cancelled, попередження |
«Оброблено :done з :total записів.» |
«усі знайдені» перевищили queued_max_all в обробнику |
failed |
«Вибрано забагато записів…» |
| завислий | failed |
як для винятку |
Сповіщення про завершення має заголовок «Дію «:action» завершено» (або «не виконано», «скасовано») і
надсилається мовою, якою користувач запустив дію. Посилання сповіщення будується під час читання із
запуску: якщо файл уже видалено, посилання зникає. Якщо модель користувача не використовує
Notifiable, сповіщення не надсилається, а запуск видно лише в «Фонових задачах».
ActionResponse::file()
public static function file(string $disk, string $path, ?string $filename = null): ActionResponse
Панель сама віддає файл з диска як завантаження (Content-Disposition: attachment,
application/octet-stream, X-Content-Type-Options: nosniff). Диск має бути в filesystems.disks, а шлях
відносним і без .., інакше кидається InvalidArgumentException. $filename за замовчуванням дорівнює
імені файлу зі шляху; керівні символи й /, \ з нього прибираються.
- У фоновій дії панель після
handle()копіює файл уlarsadmin/runs/{run}/{filename}на дискуfiles.disk(LARSADMIN_FILES_DISK, за замовчуваннямlocal) і віддає цю копію:GET {path}/runs/{run}/download, лише власнику запуску, поки копія існує.larsadmin:prune-notificationsвидаляє лише копію разом із запуском. Ваш файл панель не чіпає: тимчасовий експорт прибирайте самі (наприклад, плановим очищенням каталогу). Якщо копіювання не вдалося, помилка передається вreport(), а запуск завершується без посилання. - У синхронній дії браузер отримує посилання
GET {path}/downloads/{token}. Токен зашифровано, він прив'язаний до користувача і дійснийfiles.url_ttlхвилин (30). Файл лишається на диску: прибирайте його самі. Це посилання працює й тоді, коли сповіщення вимкнено.
Не віддавайте через file() шляхи, які складено з введення користувача: панель перевіряє лише форму
шляху, а не те, чи має користувач право на конкретний файл. Давайте файлам унікальні назви (наприклад,
Str::ulid()): із фіксованою назвою чи назвою з точністю до секунди запуски різних користувачів пишуть
в один файл, і користувач може отримати чужий експорт.
CSV і формули. Якщо експорт відкриватимуть у Excel чи LibreOffice, значення, що починається з
=,+,-,@, табуляції чи CR, виконається як формула. Додавайте до такого значення префікс':$cell = preg_match('/^[=+\-@\t\r]/', $value) === 1 ? "'".$value : $value;
Драйвер sync
З QUEUE_CONNECTION=sync (зручно для розробки) job виконується в тому самому запиті. Запуск
завершується ще до відповіді, тож користувач одразу отримує результат, як від синхронної дії
(повідомлення, перехід чи завантаження), а сповіщення теж створюється. Таймаут і скасування в sync не
діють.
Журнал аудиту
Коли журнал аудиту увімкнено, фонова дія записується так само, як синхронна: по
одному запису на кожен оброблений запис. Користувач, IP-адреса й User-Agent беруться з моменту запуску,
а не з обробника. Усі пакети запуску мають спільний batch_id, а meta кожного запису містить
ідентифікатор запуску (run). Чи писати журнал, вирішується в момент виконання.
Подія ActionRunFinished
Після того як запуск отримав кінцевий статус, диспатчиться Maxcxam\Larsadmin\Events\ActionRunFinished.
Її властивість run це модель Maxcxam\Larsadmin\Actions\Queued\ActionRun (лише для читання, payload
уже стерто):
use Illuminate\Support\Facades\Event;
use Maxcxam\Larsadmin\Actions\Queued\ActionRun;
use Maxcxam\Larsadmin\Events\ActionRunFinished;
Event::listen(function (ActionRunFinished $event) {
if ($event->run->status === ActionRun::FAILED) {
logger()->warning('Larsadmin run failed', $event->run->only('id', 'entity', 'action', 'user_id', 'message'));
}
});
Виняток у слухачі передається в report() і статус запуску не змінює.
Realtime
Без додаткового налаштування дзвіночок опитує сервер. Якщо застосунок уже використовує Reverb або Pusher, панель отримує сигнали миттєво.
Коли вмикається. notifications.realtime дорівнює auto (за замовчуванням): realtime працює, коли
сповіщення увімкнено, а з'єднання broadcasting за замовчуванням має драйвер reverb або pusher і
непорожній key. LARSADMIN_REALTIME=false вимикає його, true вмикає примусово (з іншим драйвером
realtime лишається вимкненим, а в лог один раз пишеться попередження).
Ізоляція від застосунку.
- Панель відкриває власне з'єднання лише на своїх сторінках (
/{path}/...) і закриває його при виході. Сторінки застосунку з'єднання панелі не отримують. window.Echoіwindow.Pusherзастосунку панель не читає й не змінює.laravel-echoіpusher-jsвбудовано в окремий файлdist/chunks/echo-*.js, який браузер завантажує лише тоді, коли realtime увімкнено. Встановлювати їх у застосунок не треба.
Канал. Кожен користувач має приватний канал private-larsa_inbox.{token}, де token це HMAC-SHA256
з APP_KEY від мітки призначення (larsadmin.inbox), типу й ключа користувача (40 символів).
Ідентифікатор користувача в назві каналу не розкривається, а однакові ключі різних моделей не
збігаються. Зміна APP_KEY змінює всі токени: відкриті вкладки втрачають realtime до перезавантаження,
опитування працює далі.
- Канали панелі авторизує лише ендпоінт панелі
POST {path}/broadcasting/auth(сесія guard-а панелі, CSRF,viewLarsadmin). Він відхиляє з 403 будь-який канал без префікса панелі. - Стандартний
/broadcasting/authзастосунку каналиlarsa_*не авторизує, навіть для їхнього власника. Канали застосунку пакет не реєструє й не змінює. - Префікс задає
LARSADMIN_CHANNEL_PREFIX(за замовчуваннямlarsa_): малі латинські літери, цифри й_, починається з літери, закінчується на_, до 32 символів. Інше значення кидаєInvalidArgumentException. Змініть префікс, якщо кілька застосунків ділять один app Reverb чи Pusher, і не використовуйте його для каналів застосунку.
Сигнали. Подія .larsadmin.signal несе лише ідентифікатори й числа прогресу:
{ "kind": "notification", "id": "…" } або
{ "kind": "run", "id": "…", "status": "running", "processed": 400, "total": 1200 }. Заголовків,
текстів і посилань у сигналі немає: їх клієнт дочитує з панелі. Тому сторонній сервіс (Pusher) бачить
лише ідентифікатори. Сигнали шлються одразу (ShouldBroadcastNow), без черги; прогрес не частіше раза
на секунду. Якщо broadcaster недоступний, помилка передається в report(), а сповіщення й запуск не
страждають. Клієнт вважається підключеним лише після успішної підписки на канал. Якщо підписка не вдалася або
з'єднання обірвалося, дзвіночок працює далі на звичайному опитуванні.
Приклад: Reverb
composer require laravel/reverb
php artisan reverb:install
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=...
REVERB_APP_KEY=...
REVERB_APP_SECRET=...
REVERB_HOST=ws.example.com
REVERB_PORT=443
REVERB_SCHEME=https
php artisan reverb:start
Браузер підключається до broadcasting.connections.reverb.options (REVERB_HOST, REVERB_PORT,
REVERB_SCHEME). Внутрішній хост (reverb, localhost, приватна IP-адреса, імена на .local чи
.internal) у браузер не передається ніколи: тоді клієнт не отримує хоста. Тож якщо REVERB_HOST
внутрішній (наприклад, Reverb за nginx-проксі), задайте публічну адресу окремо для панелі:
LARSADMIN_REALTIME_HOST=ws.example.com
LARSADMIN_REALTIME_PORT=443
LARSADMIN_REALTIME_SCHEME=https
Для Pusher задайте BROADCAST_CONNECTION=pusher і ключі PUSHER_* як для будь-якого застосунку Laravel;
кластер і TLS беруться з broadcasting.connections.pusher.options. Секрет і app_id у браузер не
передаються ніколи.
CSP
Якщо застосунок задає Content-Security-Policy для сторінок панелі:
script-src 'self'достатньо: файл клієнта лежить у/vendor/larsadmin/chunks/на тому самому origin;connect-srcмає містити адресу WebSocket:wss://ws.example.com:443(Reverb) абоwss://ws-{cluster}.pusher.com(Pusher). Резервні HTTP-транспорти й статистика pusher-js вимкнені, тожsockjs.pusher.comіstats.pusher.comне потрібні.
Конфігурація
config/larsadmin.php:
'actions' => [
'max_all' => 1000,
'queued_max_all' => 10000,
'queued_max_active' => 3,
'stale_after' => 3600,
'queued_stale_after' => 86400,
'queue' => env('LARSADMIN_QUEUE'),
'queue_connection' => env('LARSADMIN_QUEUE_CONNECTION'),
'runs_table' => 'larsadmin_action_runs',
'runs_connection' => env('LARSADMIN_RUNS_CONNECTION'),
],
'notifications' => [
'enabled' => env('LARSADMIN_NOTIFICATIONS', false),
'poll_interval' => 60,
'per_page' => 15,
'retention_days' => 30,
'throttle' => 120,
'realtime' => env('LARSADMIN_REALTIME', 'auto'),
'channel_prefix' => env('LARSADMIN_CHANNEL_PREFIX', 'larsa_'),
'realtime_client' => [
'host' => env('LARSADMIN_REALTIME_HOST'),
'port' => env('LARSADMIN_REALTIME_PORT'),
'scheme' => env('LARSADMIN_REALTIME_SCHEME'),
],
],
| Ключ | Змінна середовища | За замовчуванням | Опис |
|---|---|---|---|
notifications.enabled |
LARSADMIN_NOTIFICATIONS |
false |
Вмикає дзвіночок і фонові дії. Вимкнено: нові маршрути 404, $queued дії синхронні |
notifications.poll_interval |
60 |
Інтервал опитування в секундах: 0 (лише при переходах) або 15..3600 |
|
notifications.per_page |
15 |
Сповіщень на сторінку дзвіночка; 5..50 | |
notifications.retention_days |
30 |
Скільки днів зберігати для larsadmin:prune-notifications; null зберігає все |
|
notifications.throttle |
120 |
Запитів на хвилину на користувача до ендпоінтів дзвіночка, запусків і broadcasting/auth (лімітер larsadmin-notifications); null вимикає |
|
notifications.realtime |
LARSADMIN_REALTIME |
auto |
auto, true або false (див. Realtime) |
notifications.channel_prefix |
LARSADMIN_CHANNEL_PREFIX |
larsa_ |
Префікс каналів панелі |
notifications.realtime_client.* |
LARSADMIN_REALTIME_HOST, _PORT, _SCHEME |
null |
Публічна адреса WebSocket для браузера, якщо вона відрізняється від налаштувань broadcasting |
actions.queued_max_all |
10000 |
Максимум записів для фонової дії з вибором «усі знайдені» | |
actions.queued_max_active |
3 |
Активних фонових запусків на користувача | |
actions.stale_after |
3600 |
Секунд без «серцебиття», після яких запуск running без $timeout вважається завислим (плюс 60 с) |
|
actions.queued_stale_after |
86400 |
Секунд у черзі, після яких запуск queued вважається завислим |
|
actions.queue |
LARSADMIN_QUEUE |
null |
Черга за замовчуванням для фонових дій |
actions.queue_connection |
LARSADMIN_QUEUE_CONNECTION |
null |
З'єднання черги за замовчуванням |
actions.runs_table |
larsadmin_action_runs |
Таблиця запусків. Змінюйте до виконання міграції | |
actions.runs_connection |
LARSADMIN_RUNS_CONNECTION |
null |
З'єднання БД таблиці запусків |
Опублікований раніше конфіг без цих ключів працює зі значеннями за замовчуванням, тобто з вимкненою можливістю.
Міграції
| Таблиця | Звідки |
|---|---|
notifications |
стандартна таблиця Laravel: php artisan make:notifications-table. Пакет її не постачає; якщо вона вже є, нічого робити не треба |
larsadmin_action_runs |
міграція пакета: php artisan vendor:publish --tag=larsadmin-migrations |
Таблицю й з'єднання запусків міграція бере з actions.runs_table і actions.runs_connection, тож задайте
їх до migrate.
Очищення
php artisan larsadmin:prune-notifications # старші за notifications.retention_days (30 днів)
php artisan larsadmin:prune-notifications --days=7 # старші за 7 днів
Спершу команда завершує завислі запуски (з повідомленням власнику), потім видаляє пакетами по 1000:
- сповіщення панелі (тип
larsadmin), прочитані й ні. Інші сповіщення застосунку не зачіпаються; - запуски фонових дій, створені раніше за межу, зокрема завислі в статусі
queuedчиrunning; - копії файлів
ActionResponse::file()цих запусків (larsadmin/runs/{run}/…). Файли поза каталогом запуску команда не видаляє.
--days має бути цілим числом від 1. Якщо retention_days дорівнює null і --days не задано, команда
нічого не видаляє. Команда працює й тоді, коли сповіщення вимкнено; відсутню таблицю вона пропускає з
попередженням.
Пакет не реєструє розклад сам. Додайте команду в планувальник застосунку:
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('larsadmin:prune-notifications')->daily();
Безпека
- Лише свої дані. Кожен запит дзвіночка обмежено сповіщеннями типу
larsadminпоточного користувача; чуже, неіснуюче або не панельне сповіщення відповідає 404. Запуски так само: чужий запуск дає 404, а адміністратори чужих запусків не бачать. Відповідь 404 не містить назви класу моделі. - Ізоляція в обробнику. Кожен запуск отримує власну порожню сесію; хост відтвореного запиту береться
з
app.url, а не з заголовкаHost; ліміт активних запусків атомарний. - Повторна авторизація у фоновому обробнику від імені користувача, fail closed (див. Як виконується).
- Зашифровані значення. Значення полів дії (зокрема
Password) і вибір зберігаються зашифрованими й стираються після завершення запуску. У JSON запуску немає ніpayload, ні диска й шляху файлу. - Без вмісту в сигналах. Через broadcasting ідуть лише ідентифікатори й прогрес.
- XSS і перенаправлення. Тексти показуються як простий текст, рівні й іконки беруться з білого списку, посилання перевіряються під час створення й читання.
- CSRF і кеш. Зміни (
POST,DELETE) захищені CSRF групиweb; усі JSON-відповіді маютьCache-Control: no-store, private. - Персональні дані. Сповіщення й повідомлення запусків можуть містити персональні дані. Обирайте
retention_daysвідповідно до своїх вимог щодо приватності.
HTTP-ендпоінти
Усі маршрути працюють у групі auth_middleware і з вимкненими сповіщеннями відповідають 404 (крім
downloads/{token}).
| Маршрут | Ім'я | Відповідь |
|---|---|---|
GET {path}/api/notifications?cursor=… |
larsadmin.api.notifications |
{ items, unread, nextCursor, timezone } |
GET {path}/api/notifications/status |
larsadmin.api.notifications.status |
{ unread, latest, activeRuns } |
POST {path}/api/notifications/read |
larsadmin.api.notifications.read-all |
{ unread: 0 } |
POST {path}/api/notifications/{id}/read |
larsadmin.api.notifications.read |
{ unread } |
DELETE {path}/api/notifications/{id} |
larsadmin.api.notifications.destroy |
{ unread } |
GET {path}/api/runs |
larsadmin.api.runs |
{ runs }: активні й завершені за 24 години, до 10 |
GET {path}/api/runs/{run} |
larsadmin.api.runs.show |
запуск |
POST {path}/api/runs/{run}/cancel |
larsadmin.api.runs.cancel |
запуск; 409, якщо вже завершено |
POST {path}/broadcasting/auth |
larsadmin.broadcasting.auth |
підпис каналу; 404, якщо realtime вимкнено |
GET {path}/runs/{run}/download |
larsadmin.runs.download |
файл file() фонової дії, лише власнику |
GET {path}/downloads/{token} |
larsadmin.downloads.show |
файл file() синхронної дії |
Перші дев'ять маршрутів мають лімітер larsadmin-notifications, завантаження лімітер larsadmin-files.
Гість отримує 401, користувач без viewLarsadmin 403, невалідний cursor 422.
{
"items": [
{
"id": "9b2c4e1a-…",
"title": "Дію «Експорт CSV» завершено",
"body": "Дію виконано.",
"level": "success",
"icon": "download",
"url": "/admin/runs/01jc…/download",
"urlLabel": "Завантажити posts.csv",
"download": true,
"read": false,
"createdAt": "2026-11-02T14:05:11+02:00",
"run": "01jc…"
}
],
"unread": 3,
"nextCursor": null,
"timezone": "Europe/Kyiv"
}
Фронтенд отримує налаштування дзвіночка зі спільного Inertia-пропа larsadmin.notifications: null
(дзвіночка немає) або { unread, poll, urls, realtime }. unread дорівнює null, якщо таблиці немає;
realtime містить лише публічні значення (драйвер, ключ, хост, порт, канал), без секрету.
Продуктивність
| Операція | Додатково |
|---|---|
| Сторінка панелі з увімкненими сповіщеннями | 1 COUNT непрочитаних |
Опитування status |
1 COUNT непрочитаних, 1 MAX(created_at), 1 COUNT активних запусків |
| Постановка фонової дії | 1 COUNT активних запусків, 1 COUNT вибору, 1 INSERT і dispatch |
| Кожен пакет у обробнику | як у синхронній дії, плюс 1 UPDATE прогресу й 1 SELECT прапорця скасування |
| Сповіщення вимкнено | нічого |
Таблиця запусків має індекси для запусків користувача та для очищення. Основний app.js не росте на
клієнт realtime: його файл завантажується окремо й лише тоді, коли realtime увімкнено.
Обмеження
- Дзвіночок показує лише сповіщення типу
larsadmin. Інші сповіщення застосунку в панелі не видно. - Сповіщення бачить лише модель користувача guard-а панелі з
Notifiable. - Адміністратори не бачать і не можуть скасувати чужі фонові запуски.
- Поля
FileіImageу діях, зокрема фонових, не підтримуються. - Фонова дія не має автоматичних повторів. Перезапустіть її вручну, якщо вона завершилася помилкою.
- З драйвером
syncне діють таймаут і скасування. - Сесія, cookies і заголовки оригінального запиту в
handle()фонової дії недоступні. - Таймаут для визначення завислого запуску читається із зашифрованого
payload, а не з окремої колонки. - Завислий запуск визначається лише тоді, коли його хтось перевіряє (ліміт, опитування, список запусків,
larsadmin:prune-notifications). Плануйтеlarsadmin:prune-notificationsщодня.
Далі: Розширення: публічний API.