mirror of https://github.com/MarceauKa/shaark.git
✨ Health checks for links #79
This commit is contained in:
parent
5f696dee3b
commit
fd2acc7d07
|
@ -8,9 +8,7 @@
|
|||
/vendor
|
||||
.env
|
||||
.phpunit.result.cache
|
||||
composer.lock
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
package-lock.json
|
||||
yarn-error.log
|
||||
|
|
|
@ -2,51 +2,27 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Events\LinkHealthCheck;
|
||||
use App\Link;
|
||||
use App\Services\Shaark\Shaark;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class LinkHealthChecks extends Command
|
||||
{
|
||||
protected $signature = 'shaark:link_health_check {--all}';
|
||||
protected $signature = 'shaark:watch-links';
|
||||
protected $description = 'Run health checks on links';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
Link::where('is_health_check_enabled', 1)
|
||||
->where(function ($query) {
|
||||
return $query->where('http_checked_at', '<', now()->subDays(app(Shaark::class)->getLinkHealthChecksAge()))
|
||||
->orWhereNull('http_checked_at');
|
||||
})
|
||||
->orderBy('http_checked_at', 'ASC')
|
||||
->when(! $this->option('all'), function($query) {
|
||||
return $query->limit(20);
|
||||
})
|
||||
->get()
|
||||
->each(function (Link $link) {
|
||||
try {
|
||||
$response = (new Client())->request('GET', $link->getUrlAttribute(), [
|
||||
'headers' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36',
|
||||
],
|
||||
'http_errors' => false,
|
||||
'timeout' => 5,
|
||||
]);
|
||||
|
||||
$link->http_status = $response->getStatusCode();
|
||||
} catch (RequestException $exception) {
|
||||
// Might happen when the domain has expired
|
||||
$link->http_status = 500;
|
||||
} finally {
|
||||
$link->http_checked_at = now();
|
||||
$link->save();
|
||||
Link::query()
|
||||
->isWatched()
|
||||
->lastCheckedBefore(now()->subDays(app(Shaark::class)->getLinkHealthChecksAge()))
|
||||
->chunk(10, function ($links) {
|
||||
/** @var Link[]|Collection $links */
|
||||
/** @var Link $link */
|
||||
foreach ($links as $link) {
|
||||
LinkHealthCheck::dispatch($link);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,27 +12,24 @@ class Kernel extends ConsoleKernel
|
|||
{
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
$this->load(__DIR__.'/Commands');
|
||||
}
|
||||
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// Reset Demo
|
||||
$schedule->command(ResetForDemo::class)
|
||||
->when(function () {
|
||||
return config('shaark.demo');
|
||||
})
|
||||
->hourly();
|
||||
$schedule->command(ResetForDemo::class)->when(function () {
|
||||
return config('shaark.demo');
|
||||
})->hourly();
|
||||
|
||||
// Clean files
|
||||
$schedule->command(CleanFiles::class)
|
||||
->hourly();
|
||||
$schedule->command(CleanFiles::class)->hourly();
|
||||
|
||||
// Make backup
|
||||
$this->scheduleBackup($schedule);
|
||||
|
||||
// Link health checks
|
||||
$this->scheduleLinkHealthChecks($schedule);
|
||||
$this->scheduleLinksWatcher($schedule);
|
||||
}
|
||||
|
||||
protected function scheduleBackup(Schedule $schedule): self
|
||||
|
@ -58,14 +55,14 @@ class Kernel extends ConsoleKernel
|
|||
return $this;
|
||||
}
|
||||
|
||||
protected function scheduleLinkHealthChecks(Schedule $schedule): self
|
||||
protected function scheduleLinksWatcher(Schedule $schedule): self
|
||||
{
|
||||
$shaark = app(Shaark::class);
|
||||
|
||||
if (false === $shaark->getLinkHealthChecksEnabled()) {
|
||||
return $this;
|
||||
if (true === $shaark->getLinkHealthChecksEnabled()) {
|
||||
$schedule->command('shaark:watch-links')->hourly();
|
||||
}
|
||||
|
||||
$schedule->command('shaark:link_health_check')->everyTenMinutes();
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Link;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
|
||||
class LinkHealthCheck
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/** @var Link $link */
|
||||
public $link;
|
||||
|
||||
public function __construct(Link $link)
|
||||
{
|
||||
$this->link = $link;
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ class LinkController extends Controller
|
|||
$parser = WebParser::parse($request->get('url'));
|
||||
|
||||
return response()->json([
|
||||
'title' => $parser->title,
|
||||
'title' => $parser->title,
|
||||
'content' => $parser->content,
|
||||
]);
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class LinkController extends Controller
|
|||
'title',
|
||||
'content',
|
||||
'url',
|
||||
'is_health_check_enabled',
|
||||
'is_watched',
|
||||
])->toArray());
|
||||
|
||||
$link->updatePreview();
|
||||
|
@ -59,7 +59,7 @@ class LinkController extends Controller
|
|||
$post->save();
|
||||
|
||||
return response()->json([
|
||||
'post' => new PostResource($post),
|
||||
'post' => new PostResource($post),
|
||||
'status' => 'created',
|
||||
]);
|
||||
}
|
||||
|
@ -70,7 +70,12 @@ class LinkController extends Controller
|
|||
$link = Link::findOrFail($id);
|
||||
$data = collect($request->validated());
|
||||
|
||||
$link->fill($data->only('title', 'content', 'url', 'is_health_check_enabled')->toArray());
|
||||
$link->fill($data->only([
|
||||
'title',
|
||||
'content',
|
||||
'url',
|
||||
'is_watched'
|
||||
])->toArray());
|
||||
$link->updatePreview();
|
||||
|
||||
$link->post->is_pinned = $data->get('is_pinned', $link->post->is_pinned);
|
||||
|
@ -85,7 +90,7 @@ class LinkController extends Controller
|
|||
$link->post->save();
|
||||
|
||||
return response()->json([
|
||||
'post' => new PostResource($link->post),
|
||||
'post' => new PostResource($link->post),
|
||||
'status' => 'updated',
|
||||
]);
|
||||
}
|
||||
|
@ -103,7 +108,7 @@ class LinkController extends Controller
|
|||
$link->post->delete();
|
||||
|
||||
return response()->json([
|
||||
'id' => $link->id,
|
||||
'id' => $link->id,
|
||||
'status' => 'deleted',
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Manage;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\LinkResource;
|
||||
use App\Link;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LinksHealthController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('demo');
|
||||
}
|
||||
|
||||
public function get(Request $request, string $type)
|
||||
{
|
||||
if (false === in_array($type, Link::HEALTH_STATUS)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$links = Link::isWatched()
|
||||
->healthStatusIs($type)
|
||||
->withPrivate($request->user('api'))
|
||||
->get();
|
||||
|
||||
return LinkResource::collection($links);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ use App\Link;
|
|||
use App\Services\Shaark\Shaark;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class LinksHealth extends Controller
|
||||
class LinksHealthController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
|
|
|
@ -27,7 +27,7 @@ class StoreLinkRequest extends FormRequest
|
|||
'required',
|
||||
'url',
|
||||
],
|
||||
'is_health_check_enabled' => [
|
||||
'is_watched' => [
|
||||
'nullable',
|
||||
],
|
||||
'is_private' => [
|
||||
|
|
|
@ -13,11 +13,11 @@ class LinkResource extends JsonResource
|
|||
'title' => $this->title,
|
||||
'content' => $this->content,
|
||||
'url' => $this->url,
|
||||
'is_health_check_enabled' => $this->is_health_check_enabled,
|
||||
'http_status' => $this->getStatusText($this->http_status),
|
||||
'http_status_color' => $this->getStatusColor($this->http_status),
|
||||
'http_checked_at' => $this->http_checked_at,
|
||||
'http_status' => $this->http_status,
|
||||
'http_status_color' => $this->getHttpStatusColor($this->http_status),
|
||||
'http_checked_at_formated' => $this->http_checked_at ? $this->http_checked_at->diffForHumans() : '',
|
||||
'permalink' => $this->permalink,
|
||||
'is_watched' => $this->is_watched,
|
||||
'is_private' => $this->post->is_private,
|
||||
'is_pinned' => $this->post->is_pinned,
|
||||
'preview' => $this->preview,
|
||||
|
@ -38,33 +38,19 @@ class LinkResource extends JsonResource
|
|||
];
|
||||
}
|
||||
|
||||
public function getStatusText($status)
|
||||
protected function getHttpStatusColor($status): ?string
|
||||
{
|
||||
if (is_null($status)) {
|
||||
return null;
|
||||
$range = (int)floor((int)$status / 100);
|
||||
|
||||
switch ($range) {
|
||||
case 3:
|
||||
return 'info';
|
||||
case 4:
|
||||
return 'warning';
|
||||
case 5:
|
||||
return 'danger';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($status == 200) {
|
||||
return 'Healthy (200)';
|
||||
}
|
||||
|
||||
if (400 <= $status and $status <= 499) {
|
||||
return 'Dead (4xx)';
|
||||
}
|
||||
|
||||
return 'Other (3xx, 5xx)';
|
||||
}
|
||||
|
||||
public function getStatusColor($status)
|
||||
{
|
||||
if ($status == 200) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (400 <= $status and $status <= 499) {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
return 'warning';
|
||||
}
|
||||
}
|
||||
|
|
95
app/Link.php
95
app/Link.php
|
@ -4,25 +4,63 @@ namespace App;
|
|||
|
||||
use App\Concerns\Models\Postable;
|
||||
use App\Services\LinkPreview\LinkPreview;
|
||||
use App\Services\Shaark\Shaark;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* @property string $int
|
||||
* @property string $title
|
||||
* @property string|null $content
|
||||
* @property string|null $preview
|
||||
* @property string|null $archive
|
||||
* @property string $url
|
||||
* @property bool $is_watched
|
||||
* @property string $http_status
|
||||
* @property Carbon|null $http_checked_at
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property string $permalink
|
||||
* @property Post $post
|
||||
* @method self|Builder isWatched()
|
||||
* @method self|Builder isNotWatched()
|
||||
* @method self|Builder lastCheckedBefore(Carbon $date)
|
||||
* @method self|Builder healthStatusIs(string $status)
|
||||
*/
|
||||
class Link extends Model
|
||||
{
|
||||
use Postable;
|
||||
|
||||
const HEALTH_STATUS_LIVE = 'live';
|
||||
const HEALTH_STATUS_DEAD = 'dead';
|
||||
const HEALTH_STATUS_ERROR = 'error';
|
||||
const HEALTH_STATUS_REDIRECT = 'redirect';
|
||||
const HEALTH_STATUS = [
|
||||
self::HEALTH_STATUS_LIVE,
|
||||
self::HEALTH_STATUS_DEAD,
|
||||
self::HEALTH_STATUS_ERROR,
|
||||
self::HEALTH_STATUS_REDIRECT,
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'content',
|
||||
'preview',
|
||||
'archive',
|
||||
'url',
|
||||
'is_health_check_enabled',
|
||||
'is_watched',
|
||||
];
|
||||
protected $appends = [
|
||||
'permalink',
|
||||
];
|
||||
protected $casts = [
|
||||
'is_watched' => 'bool',
|
||||
];
|
||||
protected $dates = [
|
||||
'http_checked_at',
|
||||
];
|
||||
protected $touches = ['post'];
|
||||
|
||||
public function getHashIdAttribute(): string
|
||||
|
@ -44,11 +82,60 @@ class Link extends Model
|
|||
return $this->attributes['url'];
|
||||
}
|
||||
|
||||
public function setIsWatchedAttribute($value): void
|
||||
{
|
||||
if (app(Shaark::class)->getLinkHealthChecksEnabled()) {
|
||||
$this->attributes['is_watched'] = false;
|
||||
}
|
||||
|
||||
$this->attributes['is_watched'] = in_array($value, ['on', true, '1', 1]) ? true : false;
|
||||
|
||||
if ($this->attributes['is_watched'] === false) {
|
||||
$this->attributes['http_status'] = null;
|
||||
$this->attributes['http_checked_at'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function scopeHashIdIs(Builder $query, string $hash): Builder
|
||||
{
|
||||
return $query->where('id', app('hashid')->decode($hash));
|
||||
}
|
||||
|
||||
public function scopeIsWatched(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_watched', 1);
|
||||
}
|
||||
|
||||
public function scopeIsNotWatched(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_watched', 0);
|
||||
}
|
||||
|
||||
public function scopeHealthStatusIs(Builder $query, string $status): Builder
|
||||
{
|
||||
switch ($status) {
|
||||
case self::HEALTH_STATUS_LIVE:
|
||||
return $query->whereBetween('http_status', [200, 299]);
|
||||
case self::HEALTH_STATUS_DEAD:
|
||||
return $query->whereBetween('http_status', [400, 499]);
|
||||
case self::HEALTH_STATUS_ERROR:
|
||||
return $query->whereBetween('http_status', [500, 599]);
|
||||
case self::HEALTH_STATUS_REDIRECT:
|
||||
return $query->whereBetween('http_status', [300, 399]);
|
||||
default:
|
||||
return $query->whereNull('http_status');
|
||||
}
|
||||
}
|
||||
|
||||
public function scopeLastCheckedBefore(Builder $query, Carbon $date): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query) use ($date) {
|
||||
return $query
|
||||
->where('http_checked_at', '<', $date->toDateTimeString())
|
||||
->orWhereNull('http_checked_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function updatePreview(): self
|
||||
{
|
||||
$preview = LinkPreview::preview($this->url);
|
||||
|
@ -72,13 +159,11 @@ class Link extends Model
|
|||
return false;
|
||||
}
|
||||
|
||||
if ($this->post->is_private
|
||||
&& auth()->check() === false) {
|
||||
if ($this->post->is_private && auth()->check() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (app('shaark')->getPrivateDownload() === true
|
||||
&& auth()->check() === false) {
|
||||
if (app('shaark')->getPrivateDownload() === true && auth()->check() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\LinkHealthCheck;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class CheckLinkHealth implements ShouldQueue
|
||||
{
|
||||
public function handle(LinkHealthCheck $event)
|
||||
{
|
||||
logger()->info(sprintf('Checking link health %d.', $event->link->id));
|
||||
|
||||
$link = $event->link;
|
||||
|
||||
try {
|
||||
$response = (new Client())->request('GET', $link->url, [
|
||||
'headers' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36',
|
||||
],
|
||||
'http_errors' => false,
|
||||
'verify' => false,
|
||||
'timeout' => 5,
|
||||
]);
|
||||
|
||||
$link->http_status = $response->getStatusCode();
|
||||
} catch (RequestException $exception) {
|
||||
// Might happen when the domain has expired
|
||||
$link->http_status = 500;
|
||||
} finally {
|
||||
$link->http_checked_at = now();
|
||||
$link->save();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,9 @@ class EventServiceProvider extends ServiceProvider
|
|||
\App\Events\LinkArchiveRequested::class => [
|
||||
\App\Listeners\MakeLinkArchive::class
|
||||
],
|
||||
\App\Events\LinkHealthCheck::class => [
|
||||
\App\Listeners\CheckLinkHealth::class
|
||||
],
|
||||
\Illuminate\Database\Events\MigrationEnded::class => [
|
||||
\App\Listeners\UpdateDatabase::class
|
||||
],
|
||||
|
|
|
@ -39,6 +39,8 @@ use Spatie\Valuestore\Valuestore;
|
|||
* @method bool getCommentsGuestAdd()
|
||||
* @method bool getCommentsModeration()
|
||||
* @method bool getCommentsNotification()
|
||||
* @method bool getLinkHealthChecksEnabled()
|
||||
* @method int getLinkHealthChecksAge()
|
||||
*/
|
||||
trait ControlsSettings
|
||||
{
|
||||
|
@ -65,7 +67,7 @@ trait ControlsSettings
|
|||
collect($this->getSettingsConfig())
|
||||
->transform(function ($item, $key) {
|
||||
return [
|
||||
'key' => $key,
|
||||
'key' => $key,
|
||||
'default' => $item['default'],
|
||||
];
|
||||
})
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
# Unreleased
|
||||
|
||||
## Added
|
||||
|
||||
- Health checks for links (thanks to [wyred](https://github.com/wyred), [#79](https://github.com/MarceauKa/shaark/pull/79))
|
||||
|
||||
## Changed
|
||||
|
||||
- Dependencies update
|
||||
- `composer.lock` and `package-lock.json` are no longer under git
|
||||
|
||||
# 1.2.42
|
||||
|
||||
|
|
|
@ -1,29 +1,22 @@
|
|||
<?php
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Factory $factory */
|
||||
|
||||
use App\Link;
|
||||
use Faker\Generator as Faker;
|
||||
|
||||
$factory->define(Link::class, function (Faker $faker) {
|
||||
$http_status_codes = [
|
||||
null,
|
||||
200,
|
||||
302,
|
||||
404,
|
||||
500,
|
||||
];
|
||||
|
||||
$http_status_code = $http_status_codes[rand(0, 4)];
|
||||
$http_checked_at = null;
|
||||
if (! is_null($http_status_code)) {
|
||||
$http_checked_at = $faker->dateTimeThisMonth;
|
||||
}
|
||||
$is_watched = $faker->boolean();
|
||||
$http_status_code = $faker->randomElement([
|
||||
null, 200, 302, 404, 500,
|
||||
]);
|
||||
|
||||
return [
|
||||
'title' => $faker->sentence,
|
||||
'content' => $faker->paragraph,
|
||||
'url' => $faker->url,
|
||||
'http_status' => $http_status_code,
|
||||
'http_checked_at' => $http_checked_at,
|
||||
'title' => $faker->sentence,
|
||||
'content' => $faker->paragraph,
|
||||
'url' => $faker->url,
|
||||
'is_watched' => $is_watched,
|
||||
'http_status' => $http_status_code,
|
||||
'http_checked_at' => $http_status_code ? $faker->dateTimeThisMonth : null,
|
||||
];
|
||||
});
|
||||
|
|
|
@ -6,32 +6,22 @@ use Illuminate\Support\Facades\Schema;
|
|||
|
||||
class AddHealthChecksToLinksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('links', function (Blueprint $table) {
|
||||
$table->boolean('is_health_check_enabled')->after('url')->default(true);
|
||||
$table->unsignedSmallInteger('http_status')->after('is_health_check_enabled')->nullable();
|
||||
$table->boolean('is_watched')->after('url')->default(true);
|
||||
$table->unsignedSmallInteger('http_status')->after('is_watched')->nullable();
|
||||
$table->timestamp('http_checked_at')->after('http_status')->nullable();
|
||||
|
||||
$table->index('http_status');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('links', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'is_health_check_enabled',
|
||||
'is_watched',
|
||||
'http_status',
|
||||
'http_checked_at',
|
||||
]);
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"/js/app.js": "/js/app.js?id=2e6be25eae938a2f629c",
|
||||
"/css/app.css": "/css/app.css?id=79f7917999b6698482d7",
|
||||
"/js/app.js": "/js/app.js?id=41c54c1d687a35950ecf",
|
||||
"/css/app.css": "/css/app.css?id=b8b03307e0cd699952d1",
|
||||
"/js/manifest.js": "/js/manifest.js?id=3c768977c2574a34506e",
|
||||
"/js/vendor.js": "/js/vendor.js?id=d01e65db03bef4cabb61"
|
||||
"/js/vendor.js": "/js/vendor.js?id=11a927bf070f02d1f22c"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="row" v-if="enabled">
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<small class="card-title font-weight-bold text-muted">{{ __('Links watched') }}</small>
|
||||
<h3 class="text-right text-capitalize">{{ stats.total }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<small class="card-title font-weight-bold text-muted">{{ __('Links not watched') }}</small>
|
||||
<h3 class="text-right text-capitalize">{{ stats.disabled }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<small class="card-title font-weight-bold text-muted">{{ __('Live (2xx)') }}</small>
|
||||
<h3 class="text-right text-capitalize" :class="{'text-success': stats.live > 0}">
|
||||
{{ stats.live }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<small class="card-title font-weight-bold text-muted">{{ __('Redirect (3xx)') }}</small>
|
||||
<h3 class="text-right text-capitalize">{{ stats.redirect }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<small class="card-title font-weight-bold text-muted">{{ __('Dead (4xx)') }}</small>
|
||||
<h3 class="text-right text-capitalize" :class="{'text-danger': stats.dead > 0}">
|
||||
{{ stats.dead }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<small class="card-title font-weight-bold text-muted">{{ __('Error (5xx)') }}</small>
|
||||
<h3 class="text-right text-capitalize" :class="{'text-danger': stats.error > 0}">
|
||||
{{ stats.error }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-heartbeat mr-1"></i>
|
||||
{{ __('Links health') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info mb-0" v-if="!enabled">
|
||||
{{ __('Health checks for links are disabled') }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-for="type in types">
|
||||
<div class="card-title">{{ type.name }}</div>
|
||||
|
||||
<div class="alert alert-info" v-if="type.loading">
|
||||
{{ __('Loading') }}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" v-if="stats[type.type] === 0">
|
||||
{{ __('No problem found') }}
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" v-else>
|
||||
<table class="table table-borderless table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('Link') }}</th>
|
||||
<th>{{ __('Status') }}</th>
|
||||
<th>{{ __('Last checked') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="link in links[type.type]">
|
||||
<td class="align-middle">
|
||||
<a :href="link.permalink">
|
||||
{{ link.title.substr(0, 40) }}
|
||||
<span v-if="link.title.lenght >= 40">...</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ link.http_status }}
|
||||
</td>
|
||||
<td>
|
||||
{{ link.http_checked_at_formated }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
stats: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
types: [{
|
||||
type: 'dead',
|
||||
name: this.__('Dead (4xx)'),
|
||||
loading: false,
|
||||
}, {
|
||||
type: 'error',
|
||||
name: this.__('Error (5xx)'),
|
||||
loading: false,
|
||||
}, {
|
||||
type: 'redirect',
|
||||
name: this.__('Redirect (3xx)'),
|
||||
loading: false,
|
||||
}],
|
||||
links: {
|
||||
dead: [],
|
||||
error: [],
|
||||
redirect: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.types.forEach((type) => {
|
||||
this.fetch(type)
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch (type) {
|
||||
let count = this.stats[type.type] || 0
|
||||
|
||||
if (count === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
type.loading = true
|
||||
|
||||
axios.get(`/api/manage/links-health/${type.type}`).then(response => {
|
||||
this.links[type.type] = response.data.data
|
||||
type.loading = false
|
||||
}).catch(error => {
|
||||
type.loading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -3,10 +3,17 @@
|
|||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-thumbtack fa-sm pr-1" v-if="link.is_pinned && !single"></i>
|
||||
<span>{{ __('Link') }}</span> — <a :href="link.permalink">{{ link.title }}</a><br>
|
||||
<span>{{ __('Link') }}</span> —
|
||||
<a :href="link.permalink" :title="link.title">{{ link.title }}</a><br>
|
||||
<a :href="link.url" class="small text-muted">{{ displayUrl }}</a>
|
||||
</h5>
|
||||
|
||||
<div :class="`alert alert-${link.http_status_color} d-flex justify-content-between align-items-center`"
|
||||
role="alert" v-if="link.http_status >= 300">
|
||||
<span>{{ __('This link seems to be broken') }} ({{ link.http_status }})</span>
|
||||
<small v-if="link.http_checked_at_formated">{{ link.http_checked_at_formated }}</small>
|
||||
</div>
|
||||
|
||||
<p class="card-content" v-html="link.content"></p>
|
||||
|
||||
<div class="card-preview mb-1" v-html="link.preview" v-if="link.preview"></div>
|
||||
|
@ -14,15 +21,12 @@
|
|||
<p class="card-text mt-1" v-if="link.tags.length > 0">
|
||||
<a v-for="tag in link.tags" class="badge badge-secondary mr-1" :href="`/tag/${tag}`">{{ tag }}</a>
|
||||
</p>
|
||||
|
||||
<p v-if="link.http_status">
|
||||
<span class="badge" :class="'badge-' + link.http_status_color">{{ __(link.http_status) }}</span>
|
||||
<small class="text-muted">{{ __('Last Checked') }}: {{ link.http_checked_at }}</small>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-footer d-flex justify-content-between">
|
||||
<span><i class="fas fa-lock pr-2" v-if="link.is_private"></i>{{ link.date_formated }}</span>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="fas fa-lock pr-2" v-if="link.is_private"></i> {{ link.date_formated }}
|
||||
</span>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-dark btn-sm dropdown-toggle"
|
||||
|
|
|
@ -42,8 +42,8 @@
|
|||
<div class="col-12 col-md-4">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="is_health_check_enabled" v-model="form.is_health_check_enabled" :disabled="loading">
|
||||
<label class="custom-control-label" for="is_health_check_enabled" dusk="link-is_health_check_enabled">{{ __('Enable Health Check?') }}</label>
|
||||
<input type="checkbox" class="custom-control-input" id="is_watched" v-model="form.is_watched" :disabled="loading">
|
||||
<label class="custom-control-label" for="is_watched" dusk="link-is-watched">{{ __("Monitor link's health?") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -139,7 +139,7 @@ let defaultLink = function () {
|
|||
url: null,
|
||||
title: null,
|
||||
content: null,
|
||||
is_health_check_enabled: true,
|
||||
is_watched: false,
|
||||
is_private: false,
|
||||
is_pinned: false,
|
||||
tags: []
|
||||
|
|
|
@ -21,18 +21,18 @@
|
|||
<tbody>
|
||||
<tr v-for="archive in archives">
|
||||
<td class="align-middle">
|
||||
<span v-if="['pdf'].indexOf(archive.extension) !== -1">
|
||||
<i class="fas fa-file-pdf mr-1"></i>
|
||||
</span>
|
||||
<span v-else-if="['mp4', 'webm', 'mpeg', 'avi', 'mkv'].indexOf(archive.extension) !== -1">
|
||||
<i class="fas fa-file-video mr-1"></i>
|
||||
</span>
|
||||
<span v-else-if="['mp3', 'wav', 'flac'].indexOf(archive.extension) !== -1">
|
||||
<i class="fas fa-file-audio mr-1"></i>
|
||||
</span>
|
||||
<span v-else>
|
||||
<i class="fas fa-file mr-1"></i>
|
||||
</span>
|
||||
<span v-if="['pdf'].indexOf(archive.extension) !== -1">
|
||||
<i class="fas fa-file-pdf mr-1"></i>
|
||||
</span>
|
||||
<span v-else-if="['mp4', 'webm', 'mpeg', 'avi', 'mkv'].indexOf(archive.extension) !== -1">
|
||||
<i class="fas fa-file-video mr-1"></i>
|
||||
</span>
|
||||
<span v-else-if="['mp3', 'wav', 'flac'].indexOf(archive.extension) !== -1">
|
||||
<i class="fas fa-file-audio mr-1"></i>
|
||||
</span>
|
||||
<span v-else>
|
||||
<i class="fas fa-file mr-1"></i>
|
||||
</span>
|
||||
|
||||
<a :href="archive.permalink">
|
||||
{{ archive.title.substr(0, 40) }}
|
||||
|
|
|
@ -240,5 +240,18 @@
|
|||
"Source code": "Quellcode",
|
||||
"RSS Feed": "RSS Feed",
|
||||
"Atom Feed": "Atom Feed",
|
||||
"All new content of :title": "Alle neuen Inhalte von :title"
|
||||
"All new content of :title": "Alle neuen Inhalte von :title",
|
||||
|
||||
"Links health": "Links Gesundheit",
|
||||
"Links watched": "Links gesehen",
|
||||
"Links not watched": "Links nicht gesehen",
|
||||
"Live (2xx)": "Live (2xx)",
|
||||
"Redirect (3xx)": "Weiterleiten (3xx)",
|
||||
"Dead (4xx)": "Tot (4xx)",
|
||||
"Error (5xx)": "Fehler (5xx)",
|
||||
"Health checks for links are disabled": "Integritätsprüfungen für Links sind deaktiviert",
|
||||
"This link seems to be broken": "Dieser Link scheint unterbrochen zu sein",
|
||||
"Monitor link's health?": "Link Gesundheit überwachen?",
|
||||
"No problem found": "Kein Problem gefunden",
|
||||
"Last checked": "Zuletzt überprüft"
|
||||
}
|
||||
|
|
|
@ -75,7 +75,13 @@ return [
|
|||
'disabled' => 'Behindert',
|
||||
'whitelist' => 'Whitelisting',
|
||||
'all' => 'Alle',
|
||||
]
|
||||
],
|
||||
|
||||
'links' => [
|
||||
'title' => 'Gesundheitsprüfungen verknüpfen',
|
||||
'health_checks_enabled' => 'Aktivieren Sie die Integritätsprüfungen',
|
||||
'health_checks_age' => 'Anzahl der Tage zwischen den Überprüfungen für jeden Link',
|
||||
],
|
||||
],
|
||||
|
||||
// Mails
|
||||
|
|
|
@ -240,5 +240,18 @@
|
|||
"Source code": "Code source",
|
||||
"RSS Feed": "Flux RSS",
|
||||
"Atom Feed": "Flux Atom",
|
||||
"All new content of :title": "Nouveau contenu de :title"
|
||||
"All new content of :title": "Nouveau contenu de :title",
|
||||
|
||||
"Links health": "Santé des liens",
|
||||
"Links watched": "Liens surveillés",
|
||||
"Links not watched": "Liens non surveillés",
|
||||
"Live (2xx)": "En ligne (2xx)",
|
||||
"Redirect (3xx)": "Redirigé (3xx)",
|
||||
"Dead (4xx)": "Mort (4xx)",
|
||||
"Error (5xx)": "Erreur (5xx)",
|
||||
"Health checks for links are disabled": "La surveillance de la santé des liens est désactivée",
|
||||
"This link seems to be broken": "Ce lien semble cassé",
|
||||
"Monitor link's health?": "Surveiller ce lien ?",
|
||||
"No problem found": "Aucun problème détecté",
|
||||
"Last checked": "Dernière vérification"
|
||||
}
|
||||
|
|
|
@ -75,7 +75,13 @@ return [
|
|||
'disabled' => 'Désactivé',
|
||||
'whitelist' => 'Liste blanche',
|
||||
'all' => 'Tous',
|
||||
]
|
||||
],
|
||||
|
||||
'links' => [
|
||||
'title' => 'Surveillance de la santé des liens',
|
||||
'health_checks_enabled' => 'Activer la surveillance',
|
||||
'health_checks_age' => 'Nombre de jour entre chaque vérification',
|
||||
],
|
||||
],
|
||||
|
||||
// Mails
|
||||
|
|
|
@ -25,8 +25,6 @@
|
|||
"Whoops, something went wrong on our servers.": "サーバー上にエラーが発生してしまった。",
|
||||
"Yes": "はい",
|
||||
"No": "いいえ",
|
||||
"Enabled": "有効",
|
||||
"Disabled": "無効",
|
||||
"Previous": "前",
|
||||
"Next": "次",
|
||||
|
||||
|
@ -244,17 +242,16 @@
|
|||
"Atom Feed": "ATOMフィード",
|
||||
"All new content of :title": ":titleの最新コンテンツ",
|
||||
|
||||
"Total": "合計",
|
||||
"Health Checks": "健康チェック",
|
||||
"Pending Checks": "チェック待ち",
|
||||
"Healthy (200)": "健康(200)",
|
||||
"Dead (4xx)": "URL切れ(4xx)",
|
||||
"Other (3xx, 5xx)": "他(3xx、5xx)",
|
||||
"Dead Links": "切れたURL",
|
||||
"Disabled Links": "健康チェック無効のURL",
|
||||
"No dead links found": "切れたURLはありません",
|
||||
"Other-status Links": "他ステータスのURL",
|
||||
"No other-status links found": "他ステータスのURLはありません",
|
||||
"Last Checked": "最後のチェック",
|
||||
"Enable Health Check?": "健康チェックを有効しますか?"
|
||||
"Links health": "健康をリンク",
|
||||
"Links watched": "視聴したリンク",
|
||||
"Links not watched": "視聴されなかったリンク",
|
||||
"Live (2xx)": "ライブ(2xx)",
|
||||
"Redirect (3xx)": "リダイレクト(3xx)",
|
||||
"Dead (4xx)": "デッド(4xx)",
|
||||
"Error (5xx)": "エラー(5xx)",
|
||||
"Health checks for links are disabled": "リンクのヘルスチェックが無効になっています",
|
||||
"This link seems to be broken": "このリンクは壊れているようです",
|
||||
"Monitor link's health?": "リンクの状態を監視しますか?",
|
||||
"No problem found": "問題は見つかりませんでした",
|
||||
"Last checked": "最後のチェック"
|
||||
}
|
||||
|
|
|
@ -264,5 +264,18 @@
|
|||
"Source code": "Broncode",
|
||||
"RSS Feed": "RSS Feed",
|
||||
"Atom Feed": "Atom Feed",
|
||||
"All new content of :title": "Alle nieuwe inhoud van :title"
|
||||
"All new content of :title": "Alle nieuwe inhoud van :title",
|
||||
|
||||
"Links health": "Verbindt gezondheid",
|
||||
"Links watched": "Links bekeken",
|
||||
"Links not watched": "Links niet bekeken",
|
||||
"Live (2xx)": "Live (2xx)",
|
||||
"Redirect (3xx)": "Omleiding (3xx)",
|
||||
"Dead (4xx)": "Dood (4xx)",
|
||||
"Error (5xx)": "Fout (5xx)",
|
||||
"Health checks for links are disabled": "Statuscontroles voor koppelingen zijn uitgeschakeld",
|
||||
"This link seems to be broken": "Deze link lijkt te zijn verbroken",
|
||||
"Monitor link's health?": "Link's gezondheid bewaken?",
|
||||
"No problem found": "Geen probleem gevonden",
|
||||
"Last checked": "Laatst gecontroleerd"
|
||||
}
|
||||
|
|
|
@ -76,7 +76,13 @@ return [
|
|||
'disabled' => 'Uitgeschakeld',
|
||||
'whitelist' => 'White-listing',
|
||||
'all' => 'Allemaal',
|
||||
]
|
||||
],
|
||||
|
||||
'links' => [
|
||||
'title' => 'Link Health Checks',
|
||||
'health_checks_enabled' => 'Schakel gezondheidscontroles in',
|
||||
'health_checks_age' => 'Aantal dagen tussen controles voor elke link',
|
||||
],
|
||||
],
|
||||
|
||||
// Mails
|
||||
|
|
|
@ -13,11 +13,10 @@
|
|||
<div class="list-group">
|
||||
<a href="{{ route('manage.settings') }}"
|
||||
class="list-group-item list-group-item-action{{ request()->is('manage/settings') ? ' active' : '' }}"
|
||||
><i class="fas fa-fw fa-cogs mr-1"></i> {{ __('Settings') }}
|
||||
</a>
|
||||
<a href="{{ route('manage.links') }}"
|
||||
class="list-group-item list-group-item-action{{ request()->is('manage/links', 'manage/links/*') ? ' active' : '' }}"
|
||||
><i class="fas fa-fw fa-link mr-1"></i> {{ __('Links') }}</a>
|
||||
><i class="fas fa-fw fa-cogs mr-1"></i> {{ __('Settings') }}</a>
|
||||
<a href="{{ route('manage.links-health') }}"
|
||||
class="list-group-item list-group-item-action{{ request()->is('manage/links-health') ? ' active' : '' }}"
|
||||
><i class="fas fa-fw fa-heartbeat mr-1"></i> {{ __('Links health') }}</a>
|
||||
<a href="{{ route('manage.walls') }}"
|
||||
class="list-group-item list-group-item-action{{ request()->is('manage/walls') ? ' active' : '' }}"
|
||||
><i class="fas fa-fw fa-bookmark mr-1"></i> {{ __('Walls') }}</a>
|
||||
|
@ -35,6 +34,7 @@
|
|||
><i class="fas fa-fw fa-cloud-download-alt mr-1"></i> {{ __('Export') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-8">
|
||||
@yield('content')
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@extends('layouts.manage')
|
||||
|
||||
@section('content')
|
||||
@include('manage.links_dashboard')
|
||||
<health-checks :enabled="{{ $enabled ? 'true' : 'false' }}"
|
||||
:stats="{{ json_encode($stats) }}"></health-checks>
|
||||
@endsection
|
||||
|
|
|
@ -58,6 +58,8 @@ Route::group([
|
|||
|
||||
$router->get('features/{type}', 'FeaturesController@check');
|
||||
|
||||
$router->get('links-health/{type}', 'LinksHealthController@get');
|
||||
|
||||
$router->get('users', 'UsersController@all')->name('users.all');
|
||||
$router->post('users', 'UsersController@store')->name('users.store');
|
||||
$router->get('users/{id}', 'UsersController@get')->name('users.get');
|
||||
|
|
|
@ -60,11 +60,7 @@ Route::group([
|
|||
|
||||
$router->get('walls', 'WallsController@view')->name('walls');
|
||||
|
||||
$router->get('links', 'LinksController@view')->name('links');
|
||||
$router->post('links', 'LinksController@store');
|
||||
$router->get('links/dead', 'LinksController@viewDead')->name('links.dead.view');
|
||||
$router->get('links/other', 'LinksController@viewOther')->name('links.other.view');
|
||||
$router->get('links/disabled', 'LinksController@viewDisabled')->name('links.disabled.view');
|
||||
$router->get('links-health', 'LinksHealthController@view')->name('links-health');
|
||||
|
||||
$router->get('archives', 'ArchivesController@view')->name('archives');
|
||||
|
||||
|
|
Loading…
Reference in New Issue