Перейти до вмісту
en English

Версія v1.6.5 Попередні версії: документація на GitHub

Сповіщення та фонові дії

На цій сторінці

У шапці панелі з'являється дзвіночок зі сповіщеннями користувача. Сповіщення надсилає застосунок (класом LarsadminNotification) або сама панель, коли завершується фонова дія. Дія з $queued виконується в черзі Laravel: користувач одразу отримує підтвердження, бачить прогрес у дзвіночку, може скасувати запуск, а після завершення отримує сповіщення з результатом.

Оновлення приходять опитуванням. Якщо в застосунку налаштовано Reverb або Pusher, панель отримує сигнали в реальному часі (див. Realtime).

Можливість вимкнено за замовчуванням. Поки її вимкнено, панель не робить жодних додаткових запитів, нові маршрути відповідають 404, а дії з $queued виконуються синхронно, як звичайні. Додаткових залежностей Composer чи npm не потрібно: клієнт laravel-echo і pusher-js уже вбудовано в зібрані асети панелі.

Швидкий старт

  1. Створіть таблицю notifications Laravel (якщо її ще немає) і опублікуйте міграції панелі (larsadmin:install у нових проєктах уже публікує їх):

    php artisan make:notifications-table
    php artisan vendor:publish --tag=larsadmin-migrations
    php artisan migrate
    
  2. Модель користувача панелі має використовувати трейт Notifiable (стандартний App\Models\User уже його має).

  3. Увімкніть можливість у .env:

    LARSADMIN_NOTIFICATIONS=true
    QUEUE_CONNECTION=database
    
  4. Запустіть обробник черги (у продакшні через Supervisor чи аналог). retry_after з'єднання черги має бути більшим за --timeout обробника й $timeout кожної фонової дії (див. Налаштування черги):

    php artisan queue:work --timeout=600
    
  5. Заплануйте очищення в 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 і можливість увімкнено

Як виконується

  1. Постановка. Панель перевіряє те саме, що й для синхронної дії (доступ, viewAny, canSee, валідацію полів), рахує вибрані записи і створює запуск у таблиці larsadmin_action_runs. У чергу йде один job RunQueuedAction, який несе лише ідентифікатор запуску.
  2. Виконання. Обробник атомарно «захоплює» запуск (queued → running), тож повторна доставка того самого job-а нічого не робить. Записи обробляються пакетами по chunkSize, кожен пакет у власній транзакції, як у синхронній дії. Після коміту кожного пакета оновлюється прогрес.
  3. Завершення. Запуск отримує кінцевий статус (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.

Введіть запит, щоб шукати в усіх посібниках. ↑ ↓ — вибір, Enter — відкрити.