Health checks for links #79

This commit is contained in:
MarceauKa 2020-09-08 15:16:41 +02:00
parent 5f696dee3b
commit fd2acc7d07
35 changed files with 561 additions and 191 deletions

2
.gitignore vendored
View File

@ -8,9 +8,7 @@
/vendor
.env
.phpunit.result.cache
composer.lock
Homestead.json
Homestead.yaml
npm-debug.log
package-lock.json
yarn-error.log

View File

@ -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);
}
});
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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',
]);
}

View File

@ -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);
}
}

View File

@ -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()
{

View File

@ -27,7 +27,7 @@ class StoreLinkRequest extends FormRequest
'required',
'url',
],
'is_health_check_enabled' => [
'is_watched' => [
'nullable',
],
'is_private' => [

View File

@ -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';
}
}

View File

@ -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;
}

View File

@ -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();
}
}
}

View File

@ -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
],

View File

@ -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'],
];
})

View File

@ -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

View File

@ -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,
];
});

View File

@ -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',
]);

10
public/css/app.css vendored

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

2
public/js/vendor.js vendored

File diff suppressed because one or more lines are too long

View File

@ -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"
}

View File

@ -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>

View File

@ -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> &mdash; <a :href="link.permalink">{{ link.title }}</a><br>
<span>{{ __('Link') }}</span> &mdash;
<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"

View File

@ -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: []

View File

@ -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) }}

View File

@ -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"
}

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -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)": "切れxx",
"Other (3xx, 5xx)": "他xx、xx",
"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": "最後のチェック"
}

View File

@ -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"
}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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');

View File

@ -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');