├── LICENSE.md
├── README.md
├── composer.json
├── config
└── failedjobs.php
├── package.json
├── public
├── app-dark.css
├── app.css
├── app.js
├── img
│ ├── failedjobs.svg
│ ├── favicon.png
│ └── sprite.svg
└── mix-manifest.json
├── resources
├── img
│ └── favicon.png
├── js
│ ├── app.js
│ ├── base.js
│ ├── components
│ │ ├── SchemeToggler.vue
│ │ └── Stacktrace.vue
│ ├── routes.js
│ └── screens
│ │ └── failedJobs
│ │ ├── index.vue
│ │ └── job.vue
├── sass
│ ├── _colors.scss
│ ├── app-dark.scss
│ ├── app.scss
│ ├── base.scss
│ └── syntaxhighlight.scss
└── views
│ └── layout.blade.php
├── routes
└── web.php
├── src
├── Console
│ ├── InstallCommand.php
│ └── PublishCommand.php
├── Exceptions
│ └── ForbiddenException.php
├── FailedJobs.php
├── FailedJobsApplicationServiceProvider.php
├── FailedJobsServiceProvider.php
└── Http
│ ├── Controllers
│ ├── Controller.php
│ ├── FailedJobsController.php
│ └── HomeController.php
│ └── Middleware
│ └── Authenticate.php
├── stubs
└── FailedJobsServiceProvider.stub
├── testbench.yaml
├── webpack.mix.js
└── workbench
├── app
└── Providers
│ └── FailedJobsServiceProvider.php
└── database
├── factories
└── .gitkeep
├── migrations
└── .gitkeep
└── seeders
└── DatabaseSeeder.php
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Original MIT License (as used in this project):
2 |
3 | The MIT License (MIT)
4 |
5 | Copyright (c) Taylor Otwell
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in
15 | all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | THE SOFTWARE.
24 |
25 |
26 | The MIT License (MIT)
27 |
28 | Copyright (c) Hamed Panjeh
29 |
30 | Permission is hereby granted, free of charge, to any person obtaining a copy
31 | of this software and associated documentation files (the "Software"), to deal
32 | in the Software without restriction, including without limitation the rights
33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34 | copies of the Software, and to permit persons to whom the Software is
35 | furnished to do so, subject to the following conditions:
36 |
37 | The above copyright notice and this permission notice shall be included in
38 | all copies or substantial portions of the Software.
39 |
40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
46 | THE SOFTWARE.
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # Laravel Failed Jobs
8 |
9 | ## Introduction:
10 |
11 | If you're running an application with a queue driver other than Redis or dispatch a job to
12 | [a particular connection](https://laravel.com/docs/10.x/queues#dispatching-to-a-particular-connection)
13 | other than Redis, which is exclusively supported by [Laravel Horizon](https://laravel.com/docs/10.x/horizon),
14 | you might be missing out on the elegant features that Horizon offers.
15 | Laravel Horizon is known for its elegance and a wide range of implemented features.
16 | One of its standout features is its ability to present detailed information about failed job payloads
17 | and automatically load new failed jobs.
18 |
19 | In your specific application, if you've been longing for a similar Horizon-like UI to monitor failed jobs,
20 | the Laravel-Failed-Jobs package has got you covered. This package streamlines the visualization of failed jobs,
21 | eliminating the need to connect to a database client and search through `failed_jobs` records to identify
22 | the cause of a job's failure.
23 |
24 | 
25 |
26 | ## Key Benefits:
27 |
28 | - Seamless Integration: You can seamlessly integrate the Laravel-Failed-Jobs package into your project,
29 | even if you are already using Laravel Horizon. There's no conflict between the two.
30 | - While Horizon primarily reads and writes data to Redis, Laravel-Failed-Jobs retrieves
31 | data from the failed_jobs table.
32 | - With Laravel-Failed-Jobs, you can enjoy the convenience of monitoring failed jobs in your application,
33 | regardless of your queue driver. This package brings the elegance
34 | of Laravel Horizon's failed jobs UI to your specific setup.
35 | - Remote Connection Mode: This package offers two operational modes, each with its distinct setup.
36 | The Basic mode setup enabling dashboard authentication mirror that of Laravel Horizon.
37 | Remote mode is particularly beneficial for applications functioning solely as API service providers,
38 | where there is no user model or admin user.
39 | In such scenarios, the Gate definition within `FailedJobsServiceProvider`,
40 | which typically restricts access the Laravel-Failed-Jobs dashboard in non-local environments,
41 | is not applicable.
42 | Remote mode enables access to the main application from a separate Laravel application,
43 | hosted on a distinct URL address, in both local and production environments.
44 |
45 | As you correctly understood, it's important to note that the Laravel-Failed-Jobs package focuses
46 | solely on enhancing the visualization of failed jobs and does not offer managing of failed jobs or
47 | the comprehensive set of robust features found in Laravel Horizon.
48 |
49 | ## I - Installation (basic mode):
50 | If you have already installed Laravel Horizon, you can still install Laravel-Failed-Jobs without any conflict.
51 | Also it follows the same installation process as Laravel Horizon.
52 |
53 | You may install Laravel-Failed-Jobs package into your project using the Composer package manager:
54 |
55 | ```bash
56 | composer require hpwebdeveloper/laravel-failed-jobs
57 | ```
58 |
59 | After installing Laravel-Failed-Jobs, publish the assets using the `failedjobs:install` Artisan command.
60 | ```bash
61 | php artisan failedjobs:install
62 | ```
63 | This command will automatically publish the `failedjobs` config file, `FailedJobsServiceProvider`
64 | service provider and also the resource view files into the `public/vendor/failedjobs` directory.
65 |
66 | To upgrade the package, you need to use the following command after upgrading via composer:
67 |
68 | ```bash
69 | php artisan failedjobs:publish
70 | ```
71 |
72 | You may need to modify the `FailedJobsServiceProvider` to determine
73 | who can access the dashboard in production environment.
74 |
75 |
76 | ## Dashboard
77 |
78 | The Laravel-Failed-Jobs dashboard is accessible through the `/failedjobs` route by default.
79 | However, if you wish to define a new path, you can do so by setting the `FAILEDJOBS_PATH` variable in the `.env` file.
80 |
81 | ## Dashboard Authorization
82 |
83 | The`app/Providers/FailedJobsServiceProvider.php` class applies Laravel `Gate` to determine
84 | who can access FailedJobs in non-local environments. You need to follow the
85 | [same document of Horizon](https://laravel.com/docs/10.x/horizon#dashboard-authorization)
86 | to secure the dashboard in production environment.
87 |
88 | 
89 |
90 | 
91 |
92 | ## II - Installation (remote mode):
93 | Remote mode enables access to the main application from a separate Laravel application,
94 | hosted on a distinct URL address, in both local and production environments.
95 |
96 | ### Prepare package in both applications
97 | To use this package in remote mode, you need to install the package in both main and remote applications.
98 |
99 | You may install the Laravel-Failed-Jobs package into your project using the composer package manager:
100 |
101 | Note: currently the remote feature is under `feature/remote-connect` branch.
102 | ```bash
103 | composer require hpwebdeveloper/laravel-failed-jobs:dev-feature/remote-connect
104 | ```
105 | Then install the assets using the `failedjobs:install` Artisan command.
106 | ```bash
107 | php artisan failedjobs:install
108 | ```
109 |
110 | ### Prepare the environments variables
111 |
112 | In this mode you need to propertly configure the following environment variables in both applications:
113 |
114 | ```bash
115 | 'axios_base_url' => env('AXIOS_BASE_URL', ''),
116 | 'server_access_token' => env('FAILEDJOBS_SERVER_ACCESS_TOKEN'),
117 | 'dashboard_access_token' => env('FAILEDJOBS_DASHBOARD_ACCESS_TOKEN'),
118 | ```
119 |
120 | `axios_base_url` is the base URL of the main application which you have to set in the local/remote application.
121 | Setting this variable is mandatory in remote mode.
122 |
123 | `server_access_token` is the access token to access the main application from the remote application.
124 | It is mandatory to set this variable in the main application `.env` file.
125 | It is mandatory to set `dashboard_access_token` variable in the
126 | remote application equal to the value of the `server_access_token` in the main application.
127 |
128 | ### Secure the endpoint
129 | As in the `failedjobs` config file defined, the dashboard is accessible through the `/failedjobs` route
130 | by default.
131 | But it is recommended to change it when using the package in the remote mode.
132 |
133 | It is simply possible by setting the `FAILEDJOBS_PATH` variable in the `.env`
134 | file of both applications with a hash value and then define that value as a path in the `cors` config file
135 | of the main application.
136 |
137 | Modifying the `cors` config file in the main application is required because in the remote mode we are
138 | dealing with two different applications served in two different URLs.
139 | Laravel automatically respond to Cross-Origin Resource Sharing (CORS)
140 | OPTIONS HTTP requests with values that you configure in the `cors` config file.
141 | Read more about [CORS](https://laravel.com/docs/10.x/routing#cors).
142 |
143 | Hence in summary
144 | - Set the `FAILEDJOBS_PATH` variable in the `.env` file of the **main** application
145 | with a hash value like `failedjobs_4a5b6c7d`
146 | - Set the `FAILEDJOBS_PATH` variable in the `.env` file of the **remote** application
147 | with a hash value like `failedjobs_4a5b6c7d`
148 | - Open the `cors.php` config file of the main application and modify the following code accordingly:
149 | ```php
150 | // before
151 | 'paths' => ['api/*', 'sanctum/csrf-cookie'],
152 | // after
153 | 'paths' => ['failedjobs_4a5b6c7d/*', 'api/*', 'sanctum/csrf-cookie'],
154 | ```
155 |
156 | Then access the dashboard using the following URL: `http://your-local-application.test/failedjobs_4a5b6c7d`.
157 |
158 | As your main application is configured like `APP_ENV=production`
159 | the Failed-Jobs dashboard is not accessible in the production environment.
160 | While you can access the dashboard in the local environment through this URL: `http://your-local-application.test/failedjobs_4a5b6c7d`.
161 |
162 | Of course you can access the dashboard in the main application
163 | if you set the `axios_base_url` variable in the main application `.env` file as well.
164 |
165 | ## Licensing
166 |
167 | This repository uses two licenses:
168 |
169 | - The original codebase is distributed under the MIT License (MIT) (Copyright (c) Taylor Otwell),
170 | which you can find in the [LICENSE](https://github.com/HPWebdeveloper/laravel-failed-jobs/blob/main/LICENSE.md) file.
171 |
172 | - Any modifications made to the original codebase are subject to our own license,
173 | which you can find in the [LICENSE](https://github.com/HPWebdeveloper/laravel-failed-jobs/blob/main/LICENSE.md) file.
174 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hpwebdeveloper/laravel-failed-jobs",
3 | "description": "UI for Laravel failed jobs.",
4 | "keywords": ["laravel", "failed", "jobs"],
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Hamed Panjeh",
9 | "email": "panjeh@gmail.com"
10 | }
11 | ],
12 | "require": {
13 | "php": "^7.3|^8.0",
14 | "ext-json": "*",
15 | "ext-pcntl": "*",
16 | "ext-posix": "*",
17 | "illuminate/contracts": "^8.17|^9.0|^10.0|^11.0",
18 | "illuminate/queue": "^8.17|^9.0|^10.0|^11.0",
19 | "illuminate/support": "^8.17|^9.0|^10.0|^11.0",
20 | "nesbot/carbon": "^2.17",
21 | "ramsey/uuid": "^4.0",
22 | "symfony/process": "^5.0|^6.0|^7.0",
23 | "symfony/error-handler": "^5.0|^6.0|^7.0"
24 | },
25 | "require-dev": {
26 | "mockery/mockery": "^1.0",
27 | "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0",
28 | "phpstan/phpstan": "^1.10",
29 | "phpunit/phpunit": "^9.0|^10.4",
30 | "predis/predis": "^1.1|^2.0"
31 | },
32 | "suggest": {
33 | "ext-redis": "Required to use the Redis PHP driver.",
34 | "predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0)."
35 | },
36 | "autoload": {
37 | "psr-4": {
38 | "HPWebdeveloper\\LaravelFailedJobs\\": "src/"
39 | }
40 | },
41 | "autoload-dev": {
42 | "psr-4": {
43 | "HPWebdeveloper\\LaravelFailedJobs\\Tests\\": "tests/",
44 | "Workbench\\App\\": "workbench/app/",
45 | "Workbench\\Database\\Factories\\": "workbench/database/factories/",
46 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
47 | }
48 | },
49 | "extra": {
50 | "laravel": {
51 | "providers": [
52 | "HPWebdeveloper\\LaravelFailedJobs\\FailedJobsServiceProvider"
53 | ],
54 | "aliases": {
55 | "FailedJobs": "HPWebdeveloper\\LaravelFailedJobs\\FailedJobs"
56 | }
57 | }
58 | },
59 | "config": {
60 | "sort-packages": true
61 | },
62 | "minimum-stability": "dev",
63 | "prefer-stable": true,
64 | "scripts": {
65 | "lint": [
66 | "@php vendor/bin/phpstan analyse"
67 | ],
68 | "test": [
69 | "@php vendor/bin/phpunit"
70 | ]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/config/failedjobs.php:
--------------------------------------------------------------------------------
1 | env('FAILEDJOBS_DOMAIN'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | FailedJobs Path
23 | |--------------------------------------------------------------------------
24 | |
25 | | This is the URI path where FailedJobs will be accessible from. Feel free
26 | | to change this path to anything you like. Note that the URI will not
27 | | affect the paths of its internal API that aren't exposed to users.
28 | |
29 | */
30 |
31 | 'path' => env('FAILEDJOBS_PATH', 'failedjobs'),
32 |
33 | /*
34 | |--------------------------------------------------------------------------
35 | | FailedJobs Route Middleware
36 | |--------------------------------------------------------------------------
37 | |
38 | | These middleware will get attached onto each FailedJobs route, giving you
39 | | the chance to add your own middleware to this list or change any of
40 | | the existing middleware. Or, you can simply stick with this list.
41 | |
42 | */
43 |
44 | 'middleware' => ['web'],
45 |
46 | ];
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "npm run development",
5 | "development": "mix",
6 | "watch": "mix watch",
7 | "watch-poll": "mix watch -- --watch-options-poll=1000",
8 | "hot": "mix watch --hot",
9 | "prod": "npm run production",
10 | "production": "mix --production"
11 | },
12 | "devDependencies": {
13 | "axios": "^1.6.0",
14 | "bootstrap": "^4.3.1",
15 | "highlight.js": "^10.4.1",
16 | "jquery": "^3.5.0",
17 | "laravel-mix": "^6.0.13",
18 | "md5": "^2.2.1",
19 | "moment": "^2.29.4",
20 | "moment-timezone": "^0.5.35",
21 | "phpunserialize": "1.*",
22 | "popper.js": "^1.12",
23 | "resolve-url-loader": "^5.0.0",
24 | "sass": "^1.26.3",
25 | "sass-loader": "^11.0.1",
26 | "sql-formatter": "^4.0.2",
27 | "vue": "^2.5.7",
28 | "vue-json-pretty": "^1.4.1",
29 | "vue-loader": "^15.9.6",
30 | "vue-router": "^3.0.1",
31 | "vue-template-compiler": "^2.5.21"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/public/img/failedjobs.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HPWebdeveloper/laravel-failed-jobs/f539156a56c7c83caf93bb5ded3d6a579dc34776/public/img/favicon.png
--------------------------------------------------------------------------------
/public/img/sprite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/app.js": "/app.js?id=3bbefa933420c320ec2937071af50c33",
3 | "/app-dark.css": "/app-dark.css?id=2f61dc709214cae8cc7a77603b1ba389",
4 | "/app.css": "/app.css?id=43830728727b0f59dc16b9a11f09f077",
5 | "/img/failedjobs.svg": "/img/failedjobs.svg?id=904d5b5185fefb09035384e15bfca765",
6 | "/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f",
7 | "/img/sprite.svg": "/img/sprite.svg?id=afc4952b74895bdef3ab4ebe9adb746f"
8 | }
9 |
--------------------------------------------------------------------------------
/resources/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HPWebdeveloper/laravel-failed-jobs/f539156a56c7c83caf93bb5ded3d6a579dc34776/resources/img/favicon.png
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Base from './base';
3 | import axios from 'axios';
4 | import Routes from './routes';
5 | import VueRouter from 'vue-router';
6 | import VueJsonPretty from 'vue-json-pretty';
7 |
8 | window.Popper = require('popper.js').default;
9 |
10 | try {
11 | window.$ = window.jQuery = require('jquery');
12 |
13 | require('bootstrap');
14 | } catch (e) {}
15 |
16 | let token = document.head.querySelector('meta[name="csrf-token"]');
17 |
18 | axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
19 |
20 | if (token) {
21 | axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
22 | }
23 |
24 | Vue.use(VueRouter);
25 |
26 | Vue.prototype.$http = axios.create();
27 |
28 | window.FailedJobs.basePath = '/' + window.FailedJobs.path;
29 |
30 | let routerBasePath = window.FailedJobs.basePath + '/';
31 |
32 | if (window.FailedJobs.path === '' || window.FailedJobs.path === '/') {
33 | routerBasePath = '/';
34 | window.FailedJobs.basePath = '';
35 | }
36 |
37 | const router = new VueRouter({
38 | routes: Routes,
39 | mode: 'history',
40 | base: routerBasePath,
41 | });
42 |
43 | Vue.component('vue-json-pretty', VueJsonPretty);
44 | Vue.component('scheme-toggler', require('./components/SchemeToggler.vue').default);
45 |
46 | Vue.mixin(Base);
47 |
48 | Vue.directive('tooltip', function (el, binding) {
49 | $(el).tooltip({
50 | title: binding.value,
51 | placement: binding.arg,
52 | trigger: 'hover',
53 | });
54 | });
55 |
56 | new Vue({
57 | el: '#failedJobs',
58 |
59 | router,
60 |
61 | data() {
62 | return {
63 | autoLoadsNewEntries: localStorage.autoLoadsNewEntries === '1',
64 | };
65 | },
66 | });
67 |
--------------------------------------------------------------------------------
/resources/js/base.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment-timezone';
2 |
3 | export default {
4 |
5 | methods: {
6 | /**
7 | * Format the given date with respect to timezone.
8 | */
9 | formatDate(unixTime) {
10 | return moment(unixTime * 1000).add(new Date().getTimezoneOffset() / 60);
11 | },
12 |
13 | /**
14 | * Format the given date with respect to timezone.
15 | */
16 | formatDateIso(date) {
17 | return moment(date).add(new Date().getTimezoneOffset() / 60);
18 | },
19 |
20 | /**
21 | * Autoload new entries in listing screens.
22 | */
23 | autoLoadNewEntries() {
24 | if (!this.autoLoadsNewEntries) {
25 | this.autoLoadsNewEntries = true;
26 | localStorage.autoLoadsNewEntries = 1;
27 | } else {
28 | this.autoLoadsNewEntries = false;
29 | localStorage.autoLoadsNewEntries = 0;
30 | }
31 | },
32 |
33 | /**
34 | * Convert to human readable timestamp.
35 | */
36 | readableTimestamp(timestamp) {
37 | return this.formatDate(timestamp).format('YYYY-MM-DD HH:mm:ss');
38 | },
39 |
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/resources/js/components/SchemeToggler.vue:
--------------------------------------------------------------------------------
1 |
52 |
53 |
54 |
65 |
66 |
--------------------------------------------------------------------------------
/resources/js/components/Stacktrace.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |