mirror of
https://github.com/henrywhitaker3/Speedtest-Tracker.git
synced 2025-12-21 13:23:04 +01:00
Added failure graph and graph settings panel
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Speedtest Tracker
|
||||
|
||||
[](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [](https://github.com/henrywhitaker3/Speedtest-Tracker/commits)  [](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE)
|
||||
[](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [](https://github.com/henrywhitaker3/Speedtest-Tracker/commits)  [](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE)
|
||||
|
||||
This program runs a speedtest check every hour and graphs the results. The back-end is written in [Laravel](https://laravel.com/) and the front-end uses [React](https://reactjs.org/). It uses the [Ookla's speedtest cli](https://www.speedtest.net/apps/cli) package to get the data and uses [Chart.js](https://www.chartjs.org/) to plot the results.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Setting;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class SettingsHelper {
|
||||
|
||||
@@ -38,6 +39,9 @@ class SettingsHelper {
|
||||
$setting = SettingsHelper::get($name);
|
||||
|
||||
if($setting !== false) {
|
||||
if($value == false) {
|
||||
$value = "0";
|
||||
}
|
||||
$setting->value = $value;
|
||||
$setting->save();
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Helpers;
|
||||
use App\Speedtest;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use JsonException;
|
||||
@@ -63,6 +64,8 @@ class SpeedtestHelper {
|
||||
return false;
|
||||
}
|
||||
|
||||
Cache::flush();
|
||||
|
||||
return $test;
|
||||
}
|
||||
|
||||
@@ -207,4 +210,47 @@ class SpeedtestHelper {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a percentage rate of failure by days
|
||||
*
|
||||
* @param integer $days number of days to get rate for
|
||||
* @return integer percentage fail rate
|
||||
*/
|
||||
public static function failureRate(int $days)
|
||||
{
|
||||
$ttl = Carbon::now()->addDays(1);
|
||||
$rate = Cache::remember('failure-rate-' . $days, $ttl, function () use ($days) {
|
||||
$range = [
|
||||
Carbon::today()
|
||||
];
|
||||
for($i = 0; $i < $days; $i++) {
|
||||
$prev = end($range);
|
||||
$new = $prev->copy()->subDays(1);
|
||||
array_push($range, $new);
|
||||
}
|
||||
|
||||
$rate = [];
|
||||
|
||||
foreach($range as $day) {
|
||||
$success = Speedtest::select(DB::raw('COUNT(id) as rate'))->whereDate('created_at', $day)->where('failed', false)->get()[0]['rate'];
|
||||
$fail = Speedtest::select(DB::raw('COUNT(id) as rate'))->whereDate('created_at', $day)->where('failed', true)->get()[0]['rate'];
|
||||
|
||||
if(( $success + $fail ) == 0) {
|
||||
$percentage = 0;
|
||||
} else {
|
||||
$percentage = round(( $fail / ( $success + $fail ) * 100 ), 1);
|
||||
}
|
||||
|
||||
array_push($rate, [
|
||||
'date' => $day->toDateString(),
|
||||
'rate' => $percentage
|
||||
]);
|
||||
}
|
||||
|
||||
return array_reverse($rate);
|
||||
});
|
||||
|
||||
return $rate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,13 @@ class SettingsController extends Controller
|
||||
|
||||
|
||||
$config = [
|
||||
'base' => SettingsHelper::getBase()
|
||||
'base' => SettingsHelper::getBase(),
|
||||
'download_upload_graph_enabled' => SettingsHelper::get('download_upload_graph_enabled'),
|
||||
'download_upload_graph_width' => SettingsHelper::get('download_upload_graph_width'),
|
||||
'ping_graph_enabled' => SettingsHelper::get('ping_graph_enabled'),
|
||||
'ping_graph_width' => SettingsHelper::get('ping_graph_width'),
|
||||
'failure_graph_enabled' => SettingsHelper::get('failure_graph_enabled'),
|
||||
'failure_graph_width' => SettingsHelper::get('failure_graph_width'),
|
||||
];
|
||||
|
||||
return $config;
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Speedtest;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
@@ -51,10 +52,43 @@ class SpeedtestController extends Controller
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = Speedtest::where('created_at', '>=', Carbon::now()->subDays($days))
|
||||
$ttl = Carbon::now()->addDays(1);
|
||||
$data = Cache::remember('speedtest-days-' . $days, $ttl, function () use ($days) {
|
||||
return Speedtest::where('created_at', '>=', Carbon::now()->subDays($days))
|
||||
->where('failed', false)
|
||||
->orderBy('created_at', 'asc')
|
||||
->get();
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'method' => 'get speedtests in last x days',
|
||||
'days' => $days,
|
||||
'data' => $data
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns speedtest failure rate going back 'x' days
|
||||
*
|
||||
* @param int $days
|
||||
* @return void
|
||||
*/
|
||||
public function fail($days)
|
||||
{
|
||||
$rule = [
|
||||
'days' => [ 'required', 'integer' ],
|
||||
];
|
||||
|
||||
$validator = Validator::make([ 'days' => $days ], $rule);
|
||||
|
||||
if($validator->fails()) {
|
||||
return response()->json([
|
||||
'method' => 'get speedtests in last x days',
|
||||
'error' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = SpeedtestHelper::failureRate($days);
|
||||
|
||||
return response()->json([
|
||||
'method' => 'get speedtests in last x days',
|
||||
|
||||
@@ -39,7 +39,7 @@ class Kernel extends HttpKernel
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:100,1',
|
||||
'throttle:200,1',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
{
|
||||
"1.7.4": [
|
||||
{
|
||||
"description": "Stopped failed tests appearing in graphs",
|
||||
"link": ""
|
||||
},
|
||||
{
|
||||
"description": "Added failure rate graph",
|
||||
"link": ""
|
||||
},
|
||||
{
|
||||
"description": "Updated dependencies",
|
||||
"link": ""
|
||||
}
|
||||
],
|
||||
"1.7.3": [
|
||||
{
|
||||
"description": "Updated dependencies",
|
||||
|
||||
@@ -7,7 +7,7 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'version' => '1.7.3',
|
||||
'version' => '1.7.4',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
70
database/migrations/2020_07_06_105930_add_graph_settings.php
Normal file
70
database/migrations/2020_07_06_105930_add_graph_settings.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use App\Setting;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddGraphSettings extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Setting::create([
|
||||
'name' => 'download_upload_graph_enabled',
|
||||
'value' => true,
|
||||
'description' => 'Enable the download/upload graph'
|
||||
]);
|
||||
|
||||
Setting::create([
|
||||
'name' => 'download_upload_graph_width',
|
||||
'value' => 6,
|
||||
'description' => 'Set the width of the download/upload graph'
|
||||
]);
|
||||
|
||||
Setting::create([
|
||||
'name' => 'ping_graph_enabled',
|
||||
'value' => true,
|
||||
'description' => 'Enable the ping graph'
|
||||
]);
|
||||
|
||||
Setting::create([
|
||||
'name' => 'ping_graph_width',
|
||||
'value' => 6,
|
||||
'description' => 'Set the width of the ping graph'
|
||||
]);
|
||||
|
||||
Setting::create([
|
||||
'name' => 'failure_graph_enabled',
|
||||
'value' => true,
|
||||
'description' => 'Enable the failure rate graph'
|
||||
]);
|
||||
|
||||
Setting::create([
|
||||
'name' => 'failure_graph_width',
|
||||
'value' => 6,
|
||||
'description' => 'Set the width of the failure rate graph'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Setting::whereIn('name', [
|
||||
'download_upload_graph_enabled',
|
||||
'download_upload_graph_width',
|
||||
'ping_graph_enabled',
|
||||
'ping_graph_width',
|
||||
'failure_graph_enabled',
|
||||
'failure_graph_width'
|
||||
])->delete();
|
||||
}
|
||||
}
|
||||
10931
public/css/app.css
vendored
10931
public/css/app.css
vendored
File diff suppressed because one or more lines are too long
4
public/css/main.css
vendored
4
public/css/main.css
vendored
@@ -55,3 +55,7 @@
|
||||
.setting-card {
|
||||
height: 270px;
|
||||
}
|
||||
|
||||
.home-graph {
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
131746
public/js/app.js
vendored
131746
public/js/app.js
vendored
File diff suppressed because one or more lines are too long
170
resources/js/components/Graphics/HistoryGraph.js
vendored
170
resources/js/components/Graphics/HistoryGraph.js
vendored
@@ -16,8 +16,16 @@ export default class HistoryGraph extends Component {
|
||||
duOptions: {},
|
||||
pingData: {},
|
||||
pingOptions: {},
|
||||
failData: {},
|
||||
failOptions: {},
|
||||
loading: true,
|
||||
interval: null,
|
||||
graph_ul_dl_enabled: true,
|
||||
graph_ul_dl_width: 6,
|
||||
graph_failure_enabled: true,
|
||||
graph_failure_width: 6,
|
||||
graph_ping_enabled: true,
|
||||
graph_ping_width: 6,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +37,7 @@ export default class HistoryGraph extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
getData = (days = this.state.days) => {
|
||||
getDLULPing = (days) => {
|
||||
var url = 'api/speedtest/time/' + days;
|
||||
|
||||
Axios.get(url)
|
||||
@@ -52,6 +60,7 @@ export default class HistoryGraph extends Component {
|
||||
],
|
||||
};
|
||||
var duOptions = {
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: (item) => `${item.yLabel} Mbit/s`,
|
||||
@@ -90,6 +99,7 @@ export default class HistoryGraph extends Component {
|
||||
],
|
||||
};
|
||||
var pingOptions = {
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: (item) => `${item.yLabel} ms`,
|
||||
@@ -149,6 +159,97 @@ export default class HistoryGraph extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
getFailure = (days) => {
|
||||
var url = 'api/speedtest/fail/' + days;
|
||||
Axios.get(url)
|
||||
.then((resp) => {
|
||||
var failData = {
|
||||
labels: [],
|
||||
datasets:[
|
||||
{
|
||||
data: [],
|
||||
label: 'Failure',
|
||||
borderColor: "#E74C3C",
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
var failOptions = {
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: (item) => `${item.yLabel} %`,
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
text: 'Ping results for the last ' + days + ' days',
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: false,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: 'DateTime'
|
||||
}
|
||||
}],
|
||||
},
|
||||
elements: {
|
||||
point:{
|
||||
radius: 0,
|
||||
hitRadius: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp.data.data.forEach(e => {
|
||||
var date = new Date(e.date);
|
||||
var fail = {
|
||||
t: date,
|
||||
y: e.rate
|
||||
};
|
||||
failData.datasets[0].data.push(fail);
|
||||
failData.labels.push(date.getFullYear() + '/' + ('0' + (date.getMonth() + 1)).slice(-2) + '/' + ('0' + date.getDay()).slice(-2));
|
||||
});
|
||||
|
||||
this.setState({
|
||||
failData: failData,
|
||||
failOptions: failOptions
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
})
|
||||
}
|
||||
|
||||
getData = (days = this.state.days) => {
|
||||
Axios.get('api/settings/config')
|
||||
.then((resp) => {
|
||||
var data = resp.data;
|
||||
console.log(data)
|
||||
this.setState({
|
||||
graph_ul_dl_enabled: Boolean(Number(data.download_upload_graph_enabled.value)),
|
||||
graph_ul_dl_width: data.download_upload_graph_width.value,
|
||||
graph_ping_enabled: Boolean(Number(data.ping_graph_enabled.value)),
|
||||
graph_ping_width: data.ping_graph_width.value,
|
||||
graph_failure_enabled: Boolean(Number(data.failure_graph_enabled.value)),
|
||||
graph_failure_width: data.failure_graph_width.value,
|
||||
});
|
||||
|
||||
if(this.state.graph_ul_dl_enabled || this.state.graph_ping_enabled) {
|
||||
this.getDLULPing(days);
|
||||
}
|
||||
|
||||
if(this.state.graph_failure_enabled) {
|
||||
this.getFailure(days);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('Couldn\'t get the site config');
|
||||
console.log(err);
|
||||
})
|
||||
}
|
||||
|
||||
updateDays = (e) => {
|
||||
var days = e.target.value;
|
||||
if(days) {
|
||||
@@ -169,8 +270,44 @@ export default class HistoryGraph extends Component {
|
||||
var duOptions = this.state.duOptions;
|
||||
var pingData = this.state.pingData;
|
||||
var pingOptions = this.state.pingOptions;
|
||||
var failData = this.state.failData;
|
||||
var failOptions = this.state.failOptions;
|
||||
var days = this.state.days;
|
||||
|
||||
console.log(failData);
|
||||
console.log(failOptions);
|
||||
console.log(pingData);
|
||||
console.log(pingOptions);
|
||||
|
||||
var graph_ul_dl_enabled = this.state.graph_ul_dl_enabled;
|
||||
var graph_ul_dl_width = this.state.graph_ul_dl_width;
|
||||
var graph_ping_enabled = this.state.graph_ping_enabled;
|
||||
var graph_ping_width = this.state.graph_ping_width;
|
||||
var graph_failure_enabled = this.state.graph_failure_enabled;
|
||||
var graph_failure_width = this.state.graph_failure_width;
|
||||
|
||||
var dlClasses = 'my-2 home-graph ';
|
||||
var pingClasses = 'my-2 home-graph ';
|
||||
var failureClasses = 'my-2 home-graph ';
|
||||
|
||||
if(graph_ul_dl_enabled == true) {
|
||||
//
|
||||
} else {
|
||||
dlClasses += 'd-none ';
|
||||
}
|
||||
|
||||
if(graph_ping_enabled == true) {
|
||||
//
|
||||
} else {
|
||||
pingClasses += 'd-none ';
|
||||
}
|
||||
|
||||
if(graph_failure_enabled == true) {
|
||||
//
|
||||
} else {
|
||||
failureClasses += 'd-none ';
|
||||
}
|
||||
|
||||
if(loading) {
|
||||
return (
|
||||
<div>
|
||||
@@ -180,31 +317,43 @@ export default class HistoryGraph extends Component {
|
||||
} else {
|
||||
return (
|
||||
<Container className="mb-4 mt-1" fluid>
|
||||
|
||||
<Row>
|
||||
<Col
|
||||
lg={{ span: 6 }}
|
||||
md={{ span: 12 }}
|
||||
lg={{ span: graph_ul_dl_width }}
|
||||
md={{ span: graph_ul_dl_width }}
|
||||
sm={{ span: 12 }}
|
||||
xs={{ span: 12 }}
|
||||
className="my-2"
|
||||
className={dlClasses}
|
||||
>
|
||||
<Card className="shadow-sm">
|
||||
<Card.Body>
|
||||
<Line data={duData} options={duOptions} />
|
||||
<Line data={duData} options={duOptions} height={440} />
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col
|
||||
lg={{ span: 6 }}
|
||||
md={{ span: 12 }}
|
||||
lg={{ span: graph_ping_width }}
|
||||
md={{ span: graph_ping_width }}
|
||||
sm={{ span: 12 }}
|
||||
xs={{ span: 12 }}
|
||||
className="my-2"
|
||||
className={pingClasses}
|
||||
>
|
||||
<Card className="shadow-sm">
|
||||
<Card.Body>
|
||||
<Line data={pingData} options={pingOptions} />
|
||||
<Line data={pingData} options={pingOptions} height={440} />
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col
|
||||
lg={{ span: graph_failure_width }}
|
||||
md={{ span: graph_failure_width }}
|
||||
sm={{ span: 12 }}
|
||||
xs={{ span: 12 }}
|
||||
className={failureClasses}
|
||||
>
|
||||
<Card className="shadow-sm">
|
||||
<Card.Body>
|
||||
<Line data={failData} options={pingOptions} height={440} />
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -217,7 +366,6 @@ export default class HistoryGraph extends Component {
|
||||
<Form.Control id="duDaysInput" className="d-inline-block mx-2" defaultValue={days} onInput={this.updateDays}></Form.Control>
|
||||
<h4 className="d-inline mb-0">days</h4>
|
||||
</div>
|
||||
{/* <p className="text-muted">This data refreshes every 10 seconds</p> */}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
22
resources/js/components/Home/SettingWithModal.js
vendored
22
resources/js/components/Home/SettingWithModal.js
vendored
@@ -116,7 +116,7 @@ export default class SettingWithModal extends Component {
|
||||
<Row key={e.obj.id} className="d-flex align-items-center">
|
||||
<Col md={{ span: 6 }} sm={{ span: 12 }}>
|
||||
<Form.Group controlId={e.obj.name}>
|
||||
<Form.Check type="checkbox" label={name} defaultChecked={e.obj.value} onInput={this.updateValue} />
|
||||
<Form.Check type="checkbox" label={name} defaultChecked={Boolean(Number(e.obj.value))} onInput={this.updateValue} />
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col md={{ span: 6 }} sm={{ span: 12 }}>
|
||||
@@ -138,6 +138,26 @@ export default class SettingWithModal extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if(e.type == 'select') {
|
||||
return (
|
||||
<Row key={e.obj.id}>
|
||||
<Col md={{ span: 6 }} sm={{ span: 12 }}>
|
||||
<Form.Group controlId={e.obj.name}>
|
||||
<Form.Label>{name}</Form.Label>
|
||||
<Form.Control as="select" defaultValue={e.obj.value} onInput={this.updateValue}>
|
||||
{e.options.map((e,i) => {
|
||||
return (
|
||||
<option key={i} value={e.value}>{e.name}</option>
|
||||
)
|
||||
})}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col md={{ span: 6 }} sm={{ span: 12 }}>
|
||||
<p>{e.obj.description}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
})}
|
||||
<Button variant="primary" type="submit" onClick={this.update} >Save</Button>
|
||||
|
||||
58
resources/js/components/Home/Settings.js
vendored
58
resources/js/components/Home/Settings.js
vendored
@@ -60,6 +60,64 @@ export default class Settings extends Component {
|
||||
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
|
||||
<Setting name={e.server.name} value={e.server.value} description={e.server.description} />
|
||||
</Col>
|
||||
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
|
||||
<SettingWithModal title="Graph settings" description="Control settings for the graphs." settings={[
|
||||
{
|
||||
obj: e.download_upload_graph_enabled,
|
||||
type: 'checkbox'
|
||||
},
|
||||
{
|
||||
obj: e.download_upload_graph_width,
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
name: 'Full-width',
|
||||
'value': 12
|
||||
},
|
||||
{
|
||||
name: 'Half-width',
|
||||
'value': 6
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
obj: e.ping_graph_enabled,
|
||||
type: 'checkbox'
|
||||
},
|
||||
{
|
||||
obj: e.ping_graph_width,
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
name: 'Full-width',
|
||||
'value': 12
|
||||
},
|
||||
{
|
||||
name: 'Half-width',
|
||||
'value': 6
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
obj: e.failure_graph_enabled,
|
||||
type: 'checkbox'
|
||||
},
|
||||
{
|
||||
obj: e.failure_graph_width,
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
name: 'Full-width',
|
||||
'value': 12
|
||||
},
|
||||
{
|
||||
name: 'Half-width',
|
||||
'value': 6
|
||||
}
|
||||
],
|
||||
}
|
||||
]} />
|
||||
</Col>
|
||||
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
|
||||
<SettingWithModal title="Notification settings" description="Control which types of notifications the server sends." settings={[
|
||||
{
|
||||
|
||||
@@ -25,6 +25,8 @@ Route::group([
|
||||
->name('speedtest.latest');
|
||||
Route::get('time/{time}', 'SpeedtestController@time')
|
||||
->name('speedtest.time');
|
||||
Route::get('fail/{time}', 'SpeedtestController@fail')
|
||||
->name('speedtest.fail');
|
||||
Route::get('run', 'SpeedtestController@run')
|
||||
->name('speedtest.run');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user