diff --git a/app/config/routes.php b/app/config/routes.php index e363f6a..f4a80be 100644 --- a/app/config/routes.php +++ b/app/config/routes.php @@ -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') ]; diff --git a/app/controller/api.php b/app/controller/api.php index 670c778..6cbd76b 100644 --- a/app/controller/api.php +++ b/app/controller/api.php @@ -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())); diff --git a/app/controller/stats.php b/app/controller/stats.php new file mode 100644 index 0000000..836be91 --- /dev/null +++ b/app/controller/stats.php @@ -0,0 +1,114 @@ +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() + ]); + } + } +} diff --git a/app/migrations/HitsModel.php b/app/migrations/HitsModel.php new file mode 100644 index 0000000..81f08d2 --- /dev/null +++ b/app/migrations/HitsModel.php @@ -0,0 +1,51 @@ +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(); + } + } \ No newline at end of file diff --git a/app/migrations/migrations.list b/app/migrations/migrations.list new file mode 100644 index 0000000..63538ff --- /dev/null +++ b/app/migrations/migrations.list @@ -0,0 +1 @@ +47f3d3306fbd25715600db55b33b1542293ab5eb593205d6f5884bcaafa5d9a4627c76af04b684962c99a9518ef6b3d10e983203f6b5a8576912445d8f285fb7 diff --git a/app/models/HitsModel.php b/app/models/HitsModel.php new file mode 100644 index 0000000..d09064b --- /dev/null +++ b/app/models/HitsModel.php @@ -0,0 +1,100 @@ += ? 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'; + } +} \ No newline at end of file diff --git a/app/resources/js/app.js b/app/resources/js/app.js index c2aead0..c5c3a50 100644 --- a/app/resources/js/app.js +++ b/app/resources/js/app.js @@ -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(); - }); \ No newline at end of file +}); \ No newline at end of file diff --git a/app/resources/sass/app.scss b/app/resources/sass/app.scss index 2eea245..81db43c 100644 --- a/app/resources/sass/app.scss +++ b/app/resources/sass/app.scss @@ -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; diff --git a/app/views/layout.php b/app/views/layout.php index 5759a39..a0645d5 100644 --- a/app/views/layout.php +++ b/app/views/layout.php @@ -10,7 +10,6 @@ -