mirror of
https://github.com/SophiaAtkinson/steamwidgets-web.git
synced 2025-06-27 14:37:41 -07:00
Stats
This commit is contained in:
@ -22,5 +22,7 @@ return [
|
||||
array('/api/query/server', 'GET', 'api@queryServerInfo'),
|
||||
array('/api/query/user', 'GET', 'api@queryUserInfo'),
|
||||
array('/api/resource/query', 'GET', 'api@queryResource'),
|
||||
array('/stats/{pw}', 'GET', 'stats@index'),
|
||||
array('/stats/query/{pw}', 'ANY', 'stats@query'),
|
||||
array('$404', 'ANY', 'error404@index')
|
||||
];
|
||||
|
@ -28,6 +28,9 @@ class ApiController extends BaseController {
|
||||
|
||||
$data = SteamApp::querySteamData($appid, $language);
|
||||
|
||||
//Save hit
|
||||
HitsModel::addHit(HitsModel::HITTYPE_MODULE_APP);
|
||||
|
||||
return json(array('code' => 200, 'appid' => $appid, 'lang' => $language, 'data' => $data));
|
||||
} catch (\Exception $e) {
|
||||
return json(array('code' => 500, 'msg' => $e->getMessage()));
|
||||
@ -47,6 +50,9 @@ class ApiController extends BaseController {
|
||||
|
||||
$data = SteamServer::querySteamData(env('STEAM_API_KEY'), $addr);
|
||||
|
||||
//Save hit
|
||||
HitsModel::addHit(HitsModel::HITTYPE_MODULE_SERVER);
|
||||
|
||||
return json(array('code' => 200, 'addr' => $addr, 'data' => $data));
|
||||
} catch (\Exception $e) {
|
||||
return json(array('code' => 500, 'msg' => $e->getMessage()));
|
||||
@ -66,6 +72,9 @@ class ApiController extends BaseController {
|
||||
|
||||
$data = SteamUser::querySteamData(env('STEAM_API_KEY'), $steamid);
|
||||
|
||||
//Save hit
|
||||
HitsModel::addHit(HitsModel::HITTYPE_MODULE_USER);
|
||||
|
||||
return json(array('code' => 200, 'steamid' => $steamid, 'data' => $data));
|
||||
} catch (\Exception $e) {
|
||||
return json(array('code' => 500, 'msg' => $e->getMessage()));
|
||||
|
114
app/controller/stats.php
Normal file
114
app/controller/stats.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Stats controller
|
||||
*/
|
||||
class StatsController extends BaseController
|
||||
{
|
||||
const INDEX_LAYOUT = 'layout';
|
||||
|
||||
/**
|
||||
* Perform base initialization
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(self::INDEX_LAYOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles URL: /stats/{pw}
|
||||
*
|
||||
* @param Asatru\Controller\ControllerArg $request
|
||||
* @return Asatru\View\ViewHandler
|
||||
*/
|
||||
public function index($request)
|
||||
{
|
||||
if ($request->arg('pw') !== env('APP_STATSPASSWORD')) {
|
||||
throw new Exception('Invalid password');
|
||||
}
|
||||
|
||||
$start = date('Y-m-d', strtotime('-30 days'));
|
||||
$end = date('Y-m-d', strtotime('-1 day'));
|
||||
|
||||
$predefined_dates = [
|
||||
'Last week' => date('Y-m-d', strtotime('-7 days')),
|
||||
'Last two weeks' => date('Y-m-d', strtotime('-14 days')),
|
||||
'Last month' => date('Y-m-d', strtotime('-1 month')),
|
||||
'Last three months' => date('Y-m-d', strtotime('-3 months')),
|
||||
'Last year' => date('Y-m-d', strtotime('-1 year')),
|
||||
'Lifetime' => date('Y-m-d', strtotime(HitsModel::getInitialStartDate()))
|
||||
];
|
||||
|
||||
return parent::view([
|
||||
['content', 'stats'],
|
||||
], [
|
||||
'render_stats_to' => 'hits-stats',
|
||||
'render_stats_start' => $start,
|
||||
'render_stats_end' => $end,
|
||||
'render_stats_pw' => $request->arg('pw'),
|
||||
'predefined_dates' => $predefined_dates
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles URL: /stats/query/{pw}
|
||||
*
|
||||
* @param Asatru\Controller\ControllerArg $request
|
||||
* @return Asatru\View\JsonHandler
|
||||
*/
|
||||
public function query($request)
|
||||
{
|
||||
try {
|
||||
if ($request->arg('pw') !== env('APP_STATSPASSWORD')) {
|
||||
throw new Exception('Invalid password');
|
||||
}
|
||||
|
||||
$start = $request->params()->query('start', '');
|
||||
if ($start === '') {
|
||||
$start = date('Y-m-d', strtotime('-30 days'));
|
||||
}
|
||||
|
||||
$end = $request->params()->query('end', '');
|
||||
if ($end === '') {
|
||||
$end = date('Y-m-d', strtotime('-1 day'));
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$data[HitsModel::HITTYPE_MODULE_APP] = [];
|
||||
$data[HitsModel::HITTYPE_MODULE_SERVER] = [];
|
||||
$data[HitsModel::HITTYPE_MODULE_USER] = [];
|
||||
|
||||
$hits = HitsModel::getHitsPerDay($start, $end);
|
||||
|
||||
$dayDiff = (new DateTime($end))->diff((new DateTime($start)))->format('%a');
|
||||
|
||||
$count_total = [];
|
||||
$count_total[HitsModel::HITTYPE_MODULE_APP] = 0;
|
||||
$count_total[HitsModel::HITTYPE_MODULE_SERVER] = 0;
|
||||
$count_total[HitsModel::HITTYPE_MODULE_USER] = 0;
|
||||
|
||||
for ($i = 0; $i < $hits->count(); $i++) {
|
||||
$count_total[$hits->get($i)->get('hittype')] += $hits->get($i)->get('count');
|
||||
|
||||
$data[$hits->get($i)->get('hittype')][$hits->get($i)->get('created_at')][] = $hits->get($i)->get('count');
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'data' => $data,
|
||||
'counts' => $count_total,
|
||||
'count_total' => $count_total[HitsModel::HITTYPE_MODULE_APP] + $count_total[HitsModel::HITTYPE_MODULE_SERVER] + $count_total[HitsModel::HITTYPE_MODULE_USER],
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
'day_diff' => $dayDiff
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
51
app/migrations/HitsModel.php
Normal file
51
app/migrations/HitsModel.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
Asatru PHP - Migration for hits
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class specifies a migration
|
||||
*/
|
||||
class HitsModel_Migration {
|
||||
private $database = null;
|
||||
private $connection = null;
|
||||
|
||||
/**
|
||||
* Store the PDO connection handle
|
||||
*
|
||||
* @param \PDO $pdo The PDO connection handle
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($pdo)
|
||||
{
|
||||
$this->connection = $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the table shall be created or modified
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$this->database = new Asatru\Database\Migration('hits', $this->connection);
|
||||
$this->database->drop();
|
||||
$this->database->add('id INT NOT NULL AUTO_INCREMENT PRIMARY KEY');
|
||||
$this->database->add('hash_token VARCHAR(512) NOT NULL');
|
||||
$this->database->add('hittype VARCHAR(100) NOT NULL');
|
||||
$this->database->add('created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
|
||||
$this->database->create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the table shall be dropped
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
if ($this->database)
|
||||
$this->database->drop();
|
||||
}
|
||||
}
|
1
app/migrations/migrations.list
Normal file
1
app/migrations/migrations.list
Normal file
@ -0,0 +1 @@
|
||||
47f3d3306fbd25715600db55b33b1542293ab5eb593205d6f5884bcaafa5d9a4627c76af04b684962c99a9518ef6b3d10e983203f6b5a8576912445d8f285fb7
|
100
app/models/HitsModel.php
Normal file
100
app/models/HitsModel.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class HitsModel
|
||||
*
|
||||
* Responsible for hit calculation
|
||||
*/
|
||||
class HitsModel extends \Asatru\Database\Model
|
||||
{
|
||||
const HITTYPE_MODULE_APP = 'mod_app';
|
||||
const HITTYPE_MODULE_SERVER = 'mod_server';
|
||||
const HITTYPE_MODULE_USER = 'mod_user';
|
||||
|
||||
/**
|
||||
* Validate hit type
|
||||
*
|
||||
* @param $type
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function validateHitType($type)
|
||||
{
|
||||
try {
|
||||
$types = [self::HITTYPE_MODULE_APP, self::HITTYPE_MODULE_SERVER, self::HITTYPE_MODULE_USER];
|
||||
|
||||
if (!in_array($type, $types)) {
|
||||
throw new Exception('Invalid hit type: ' . $type);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add hit to database
|
||||
*
|
||||
* @param $address
|
||||
* @param $type
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function addHit($type)
|
||||
{
|
||||
try {
|
||||
static::validateHitType($type);
|
||||
|
||||
$token = md5($_SERVER['REMOTE_ADDR']);
|
||||
|
||||
HitsModel::raw('INSERT INTO `' . self::tableName() . '` (hash_token, hittype, created_at) VALUES(?, ?, CURRENT_TIMESTAMP)', [
|
||||
$token,
|
||||
$type
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all hits of the given range
|
||||
*
|
||||
* @param $start
|
||||
* @param $end
|
||||
* @return Asatru\Database\Collection
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function getHitsPerDay($start, $end)
|
||||
{
|
||||
try {
|
||||
$result = HitsModel::raw('SELECT DATE(created_at) AS created_at, COUNT(hash_token) AS count, hittype FROM `' . self::tableName() . '` WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? GROUP BY DATE(created_at), hittype ORDER BY created_at ASC', [
|
||||
$start,
|
||||
$end
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get initial start date
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getInitialStartDate()
|
||||
{
|
||||
$data = HitsModel::raw('SELECT created_at FROM `' . self::tableName() . '` WHERE id = 1')->first();
|
||||
return $data->get('created_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the associated table name of the migration
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function tableName()
|
||||
{
|
||||
return 'hits';
|
||||
}
|
||||
}
|
@ -4,7 +4,14 @@
|
||||
* Put here your application specific JavaScript implementations
|
||||
*/
|
||||
|
||||
window.vue = new Vue({
|
||||
import './../sass/app.scss';
|
||||
|
||||
window.axios = require('axios');
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
import Chart from 'chart.js/auto';
|
||||
|
||||
window.vue = new Vue({
|
||||
el: '#app',
|
||||
|
||||
data: {
|
||||
@ -26,6 +33,145 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ajaxRequest: function (method, url, data = {}, successfunc = function(data){}, finalfunc = function(){}, config = {})
|
||||
{
|
||||
let func = window.axios.get;
|
||||
if (method == 'post') {
|
||||
func = window.axios.post;
|
||||
} else if (method == 'patch') {
|
||||
func = window.axios.patch;
|
||||
} else if (method == 'delete') {
|
||||
func = window.axios.delete;
|
||||
}
|
||||
|
||||
func(url, data, config)
|
||||
.then(function(response){
|
||||
successfunc(response.data);
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
})
|
||||
.finally(function(){
|
||||
finalfunc();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
renderStats: function(pw, elem, start, end = '') {
|
||||
window.vue.ajaxRequest('post', window.location.origin + '/stats/query/' + pw, { start: start, end: end }, function(response){
|
||||
if (response.code == 200) {
|
||||
document.getElementById('inp-date-from').value = response.start;
|
||||
document.getElementById('inp-date-till').value = response.end;
|
||||
document.getElementById('count-total').innerHTML = response.count_total;
|
||||
document.getElementById('count-app').innerHTML = response.counts.mod_app;
|
||||
document.getElementById('count-server').innerHTML = response.counts.mod_server;
|
||||
document.getElementById('count-user').innerHTML = response.counts.mod_user;
|
||||
|
||||
let content = document.getElementById(elem);
|
||||
if (content) {
|
||||
let labels = [];
|
||||
let data_app = [];
|
||||
let data_server = [];
|
||||
let data_user = [];
|
||||
|
||||
let day = 60 * 60 * 24 * 1000;
|
||||
let dt = new Date(Date.parse(start));
|
||||
|
||||
for (let i = 0; i <= response.day_diff; i++) {
|
||||
let curDate = new Date(dt.getTime() + day * i);
|
||||
let curDay = curDate.getDate();
|
||||
let curMonth = curDate.getMonth() + 1;
|
||||
|
||||
if (curDay < 10) {
|
||||
curDay = '0' + curDay;
|
||||
}
|
||||
|
||||
if (curMonth < 10) {
|
||||
curMonth = '0' + curMonth;
|
||||
}
|
||||
|
||||
labels.push(curDate.getFullYear() + '-' + curMonth + '-' + curDay);
|
||||
data_app.push(0);
|
||||
data_server.push(0);
|
||||
data_user.push(0);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(response.data.mod_app)) {
|
||||
labels.forEach(function(lblElem, lblIndex){
|
||||
if (lblElem == key) {
|
||||
data_app[lblIndex] = parseInt(value[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(response.data.mod_server)) {
|
||||
labels.forEach(function(lblElem, lblIndex){
|
||||
if (lblElem == key) {
|
||||
data_server[lblIndex] = parseInt(value[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(response.data.mod_user)) {
|
||||
labels.forEach(function(lblElem, lblIndex){
|
||||
if (lblElem == key) {
|
||||
data_user[lblIndex] = parseInt(value[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'App',
|
||||
backgroundColor: 'rgb(0, 162, 232)',
|
||||
borderColor: 'rgb(0, 162, 232)',
|
||||
data: data_app,
|
||||
},
|
||||
{
|
||||
label: 'Server',
|
||||
backgroundColor: 'rgb(163, 73, 164)',
|
||||
borderColor: 'rgb(163, 73, 164)',
|
||||
data: data_server,
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
backgroundColor: 'rgb(240, 155, 90)',
|
||||
borderColor: 'rgb(240, 155, 90)',
|
||||
data: data_user,
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: function(value) {if (value % 1 === 0) {return value;}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (window.statsChart !== null) {
|
||||
window.statsChart.destroy();
|
||||
}
|
||||
|
||||
window.statsChart = new Chart(
|
||||
content,
|
||||
config
|
||||
);
|
||||
}
|
||||
} else {
|
||||
alert(response.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@ -36,4 +182,4 @@ window.hljs = hljs;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
window.hljs.highlightAll();
|
||||
});
|
||||
});
|
@ -109,6 +109,14 @@ body {
|
||||
background-color: rgb(200, 200, 200);
|
||||
}
|
||||
|
||||
.stats-input {
|
||||
padding: 8px;
|
||||
color: rgb(50, 50, 50);
|
||||
background-color: rgb(200, 200, 200);
|
||||
border: 1px solid rgb(100, 100, 100);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.scroll-to-top {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
|
@ -10,7 +10,6 @@
|
||||
<link rel="icon" type="image/png" href="{{ asset('img/logo.png') }}"/>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ asset('css/bulma.css') }}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ asset('css/app.css') }}"/>
|
||||
|
||||
<title>{{ env('APP_TITLE') }}</title>
|
||||
|
||||
@ -54,6 +53,11 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.vue.initNavBar();
|
||||
|
||||
@if ((isset($render_stats_to)) && (isset($render_stats_pw)))
|
||||
window.statsChart = null;
|
||||
window.vue.renderStats('{{ $render_stats_pw }}', '{{ $render_stats_to }}', '{{ $render_stats_start }}');
|
||||
@endif
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
25
app/views/stats.php
Normal file
25
app/views/stats.php
Normal file
@ -0,0 +1,25 @@
|
||||
<div class="content-section content-top-margin">
|
||||
<h1>Stats</h1>
|
||||
|
||||
<div class="hits-summary">
|
||||
<div>
|
||||
Range: <input class="stats-input" type="date" id="inp-date-from"/> <input class="stats-input" type="date" id="inp-date-till"/>
|
||||
<select class="stats-input" onchange="window.vue.renderStats('{{ $render_stats_pw }}', '{{ $render_stats_to }}', this.value, '{{ $render_stats_end }}');">
|
||||
<option value="{{ $render_stats_start }}">- Select a range -</option>
|
||||
@foreach ($predefined_dates as $key => $value)
|
||||
<option value="{{ $value }}">{{ $key }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<a class="button is-dark" href="javascript:void(0);" onclick="window.vue.renderStats('{{ $render_stats_pw }}', '{{ $render_stats_to }}', document.getElementById('inp-date-from').value, document.getElementById('inp-date-till').value);">Go</a>
|
||||
</div>
|
||||
|
||||
<div>Sum: <div class="is-inline-block" id="count-total"></div></div>
|
||||
<div>App count: <div class="is-inline-block" id="count-app"></div></div>
|
||||
<div>Server count: <div class="is-inline-block" id="count-server"></div></div>
|
||||
<div>User count: <div class="is-inline-block" id="count-user"></div></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<canvas id="hits-stats"></canvas>
|
||||
</div>
|
||||
</div>
|
Reference in New Issue
Block a user