diff --git a/.vscode/settings.json b/.vscode/settings.json index 8e7ad5e0ac7c3b3535eceb7f58a06b186503225f..8024e924dc3d1258f3dcfeeba04b54295211d0c0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,5 +26,8 @@ "**/storage/framework/views/**", "_ide_helper.php", "_ide_helper_models" - ] + ], + "emmet.includeLanguages": { + "blade": "html" + } } \ No newline at end of file diff --git a/metager/app/Http/Controllers/AdminInterface.php b/metager/app/Http/Controllers/AdminInterface.php index 5271385e61f16d5a5861f09e0fc9a7f979207c60..02598f7d2f9575349bf4ba8ddf2116103f60b1cd 100644 --- a/metager/app/Http/Controllers/AdminInterface.php +++ b/metager/app/Http/Controllers/AdminInterface.php @@ -157,6 +157,7 @@ class AdminInterface extends Controller $month = $date->format("m"); $day = $date->format("d"); $cache_key = "admin_count_total_${year}_${month}_${day}"; + Cache::forget($cache_key); $total_count = Cache::get($cache_key); if ($total_count === null) { diff --git a/metager/app/QueryLogger.php b/metager/app/QueryLogger.php index e05c93e4457f40440c0b0ef4d69d4c3dd144142c..01c7767497709640aa95eb16a4a94ef5a6c955f0 100644 --- a/metager/app/QueryLogger.php +++ b/metager/app/QueryLogger.php @@ -98,17 +98,17 @@ class QueryLogger * @var string[] $query_logs */ $query_logs = $redis->lrange(self::REDIS_KEY, 0, $queue_size - 1); - if(sizeof($query_logs) > 0){ - if(self::insertLogEntries($query_logs)){ + if (sizeof($query_logs) > 0) { + if (self::insertLogEntries($query_logs)) { Log::info("Added " . sizeof($query_logs) . " lines to todays log! "); // Now we can pop those elements from the list for ($i = 0; $i < sizeof($query_logs); $i++) { $redis->lpop(self::REDIS_KEY); } - }else{ + } else { Log::error("Konnte " . sizeof($log_strings) . " Log Zeile(n) nicht schreiben"); } - }else { + } else { Log::info("No logs to append to the file."); } } @@ -116,28 +116,29 @@ class QueryLogger /** * Inserts new Log entries into Sqlite database */ - private static function insertLogEntries($query_logs){ + private static function insertLogEntries($query_logs) + { $insert_array = []; - foreach($query_logs as $query_log){ + foreach ($query_logs as $query_log) { $query_log_object = json_decode($query_log); - if(empty($query_log_object)){ + if (empty($query_log_object)) { Log::error(var_export($query_log, true)); continue; - }else{ + } else { $query_log = $query_log_object; } $time = DateTime::createFromFormat("Y-m-d H:i:s", $query_log->time); $year = $time->format("Y"); $month = $time->format("m"); $day = $time->format("d"); - if(empty($insert_array[$year])){ + if (empty($insert_array[$year])) { $insert_array[$year] = []; } - if(empty($insert_array[$year][$month])){ + if (empty($insert_array[$year][$month])) { $insert_array[$year][$month] = []; } - if(empty($insert_array[$year][$month][$day])){ + if (empty($insert_array[$year][$month][$day])) { $insert_array[$year][$month][$day] = []; } $insert_array[$year][$month][$day][] = [ @@ -152,17 +153,17 @@ class QueryLogger /** @var \Illuminate\Database\SQLiteConnection[] */ $connections = []; - foreach($insert_array as $year => $months){ - foreach($months as $month => $days){ - foreach($days as $day => $insert_array){ - if(empty($connection[$year])){ + foreach ($insert_array as $year => $months) { + foreach ($months as $month => $days) { + foreach ($days as $day => $insert_array) { + if (empty($connection[$year])) { $connections[$year] = []; } - if(empty($connections[$year][$month])){ + if (empty($connections[$year][$month])) { $connections[$year][$month] = self::validateDatabase($year, $month); } self::validateTable($connections[$year][$month], $day); - if(!$connections[$year][$month]->table($day)->insert($insert_array)){ + if (!$connections[$year][$month]->table($day)->insert($insert_array)) { return false; } } @@ -174,18 +175,19 @@ class QueryLogger /** * Verifies that the Sqlite Database and Table for todays Log exist */ - private static function validateDatabase($year, $month) { + private static function validateDatabase($year, $month) + { $folder = \storage_path("logs/metager/$year"); - if(!\file_exists($folder)){ - if(!mkdir($folder, 0777, true)){ + if (!\file_exists($folder)) { + if (!mkdir($folder, 0777, true)) { throw new ErrorException("Couldn't create folder for sqlite Databse in \"$folder\""); } } $current_database_path = \storage_path("logs/metager/$year/$month.sqlite"); - + // Create Database if it does not exist yet - if(!\file_exists($current_database_path)){ - if(!touch($current_database_path)){ + if (!\file_exists($current_database_path)) { + if (!touch($current_database_path)) { throw new ErrorException("Couldn't create sqlite Databse in \"$current_database_path\""); } } @@ -201,10 +203,11 @@ class QueryLogger * @param Illuminate\Database\SQLiteConnection $connection * @param string $table */ - private static function validateTable($connection, $table){ - if(!$connection->getSchemaBuilder()->hasTable("$table")){ + private static function validateTable($connection, $table) + { + if (!$connection->getSchemaBuilder()->hasTable("$table")) { // Create a new Table - $connection->getSchemaBuilder()->create("$table", function(Blueprint $table){ + $connection->getSchemaBuilder()->create("$table", function (Blueprint $table) { $table->bigIncrements("id"); $table->dateTime("time"); $table->string("referer", self::REFERER_MAX_LENGTH); @@ -222,7 +225,8 @@ class QueryLogger * @param string $year * @param string $month */ - public static function migrate($year, $month){ + public static function migrate($year, $month) + { $batch_size = 10000; $path = \storage_path("logs/metager/$year/$month"); @@ -230,8 +234,8 @@ class QueryLogger $redis = Redis::connection(); $files = scandir($path); - foreach($files as $file){ - if(\in_array($file, [".", ".."])) continue; + foreach ($files as $file) { + if (\in_array($file, [".", ".."])) continue; $day = substr($file, 0, stripos($file, ".log")); Log::info("Parsing $file"); @@ -240,8 +244,8 @@ class QueryLogger \exec("mv " . $file_path . ".bak" . " " . $file_path); $fh = fopen($file_path, "r"); $batch_count = 0; - while(($line = fgets($fh)) !== false){ - if(preg_match("/^(\d{2}:\d{2}:\d{2})\s+?ref=(.*?)\s+?time=([^\s]+)\s+?serv=([^\s]+)\s+?interface=([^\s]+).*?eingabe=(.+)$/", $line, $matches) != false){ + while (($line = fgets($fh)) !== false) { + if (preg_match("/^(\d{2}:\d{2}:\d{2})\s+?ref=(.*?)\s+?time=([^\s]+)\s+?serv=([^\s]+)\s+?interface=([^\s]+).*?eingabe=(.+)$/", $line, $matches) != false) { $log_entry = [ "time" => "$year-$month-$day " . $matches[1], "referer" => trim($matches[2]), @@ -251,18 +255,18 @@ class QueryLogger "query_string" => trim($matches[6]) ]; $json_string = \json_encode($log_entry); - if($json_string === false){ + if ($json_string === false) { Log::error("Couldn't encode"); Log::error(var_export($log_entry, true)); continue; } $redis->rpush(self::REDIS_KEY, $json_string); $batch_count++; - if($batch_count >= $batch_size){ + if ($batch_count >= $batch_size) { Artisan::call("logs:gather"); $batch_count = 0; } - }else{ + } else { Log::error("Regexp did not work for"); Log::error($line); continue; @@ -272,6 +276,5 @@ class QueryLogger Log::info("Finished $file"); fclose($fh); } - } } diff --git a/metager/resources/js/admin/count.js b/metager/resources/js/admin/count.js index fba06b2fd5aff9a9ccea219bab979c69ac851783..e532ddbd8c35272d4d34ba55ab79646eaf556ab4 100644 --- a/metager/resources/js/admin/count.js +++ b/metager/resources/js/admin/count.js @@ -1,10 +1,8 @@ let parallel_fetches = 8; -let totals = []; +let data = []; -document.addEventListener("DOMContentLoaded", () => { - load(); -}); +load(); function load() { let parallel = Math.floor(parallel_fetches / 2) @@ -15,48 +13,121 @@ function load() { if (fetches.length > 0) { let allData = Promise.all(fetches) allData.then((res) => { + updateTable(); + updateRecord(); load(); }); } else { - updateMedians(); + updateTable(); + updateRecord(); } } -function updateMedians() { - let median_elements = document.querySelectorAll("tr > td.total.loading"); +function updateRecord() { + let record_total = null; + for (let i = 0; i < data.length; i++) { + let total = data[i]["total"] + let same_time = data[i]["same_time"] + let date = document.querySelectorAll("tbody tr .date")[i].dataset.date_formatted + if (typeof total === "number" && typeof same_time === "number" && (record_total === null || record_total < total)) { + record_total = total; + let record_same_time_element = document.querySelector(".record .record-same-time"); + let record_total_element = document.querySelector(".record .record-total"); + record_same_time_element.innerHTML = same_time.toLocaleString('de-DE', { + maximumFractionDigits: 0 + }); + record_same_time_element.classList.remove("loading"); + record_total_element.innerHTML = record_total.toLocaleString('de-DE', { + maximumFractionDigits: 0 + }); + record_total_element.classList.remove("loading"); + let record_date_element = document.querySelector(".record .record-date"); + record_date_element.classList.remove("loading"); + record_date_element.innerHTML = date; + } + } +} + +function updateTable() { + let sum = 0; + for (let i = 0; i < data.length; i++) { + if (typeof data[i]["total"] === "number") { + // Update Total Number + let total_element = document.querySelector("[data-days_ago=\"" + i + "\"] .total") + total_element.innerHTML = data[i]["total"].toLocaleString('de-DE', { + maximumFractionDigits: 0 + }) + total_element.classList.remove("loading") + sum += data[i]["total"] + } else { + sum = undefined; + } + if (typeof data[i]["same_time"] === "number") { + // Update Total Number + let same_time_element = document.querySelector("[data-days_ago=\"" + i + "\"] .same-time") + same_time_element.innerHTML = data[i]["same_time"].toLocaleString('de-DE', { + maximumFractionDigits: 0 + }) + same_time_element.classList.remove("loading") + } + if (typeof sum !== undefined) { + let median_element = document.querySelector("[data-days_ago=\"" + i + "\"] .median"); + let median = 0; + if (i > 0) { + median = sum / i; + } + median_element.innerHTML = median.toLocaleString('de-DE', { + maximumFractionDigits: 0 + }); + median_element.classList.remove("loading"); + let total_median_days_element = document.querySelector(".total-median .median-days"); + total_median_days_element.classList.remove("loading") + total_median_days_element.innerHTML = i + 1 + let total_median_values_element = document.querySelector(".total-median .median-value"); + total_median_values_element.classList.remove("loading") + total_median_values_element.innerHTML = median.toLocaleString('de-DE', { + maximumFractionDigits: 0 + }); + } + } } function loadTotals(parallel) { - let loading_elements = document.querySelectorAll("tr > td.median.loading"); + let loading_elements = document.querySelectorAll("tr > td.total.loading"); let fetches = []; for (let i = 0; i < loading_elements.length; i++) { let element = loading_elements[i]; let date = element.parentNode.querySelector(".date").dataset.date; - let total_requests = localStorage.getItem("totals-" + date) - if (total_requests) { - element.innerHTML = total_requests - element.classList.remove("loading"); - continue; - } + let days_ago = parseInt(element.parentNode.dataset.days_ago) - if (fetches.length < parallel) { + let total_requests = parseInt(localStorage.getItem("totals-" + date)) + if (days_ago === 0) { + if (!data[days_ago]) { + data[days_ago] = {} + } + data[days_ago]["total"] = 0; + } else if (total_requests) { + if (!data[days_ago]) { + data[days_ago] = {} + } + data[days_ago]["total"] = total_requests; + } else if (fetches.length < parallel) { fetches.push(fetch('/admin/count/count-data-total?date=' + date) .then(response => response.json()) .then(response => { - total_requests = response.data.total; - localStorage.setItem("totals-" + date, total_requests); - element.innerHTML = total_requests - element.classList.remove("loading"); - let sum = 0; - for (let j = 0; j < totals.length; j++) { - sum += totals[j]; + total_requests = parseInt(response.data.total); + if (!data[days_ago]) { + data[days_ago] = {} } - + data[days_ago]["total"] = total_requests; + localStorage.setItem("totals-" + date, total_requests); }) .catch(reason => { - element.innerHTML = "-" - element.classList.remove("loading"); + if (!data[days_ago]) { + data[days_ago] = {} + } + data[days_ago]["total"] = 0; })); } else { break; @@ -73,25 +144,29 @@ function loadSameTimes(parallel) { let element = loading_elements[i]; let date = element.parentNode.querySelector(".date").dataset.date; - let total_requests = localStorage.getItem("until-" + date) - if (total_requests) { - element.innerHTML = total_requests - element.classList.remove("loading"); - continue; - } + let days_ago = parseInt(element.parentNode.dataset.days_ago) - if (fetches.length < parallel) { + let total_requests = parseInt(localStorage.getItem("until-" + date)) + if (total_requests) { + if (!data[days_ago]) { + data[days_ago] = {} + } + data[days_ago]["same_time"] = total_requests; + } else if (fetches.length < parallel) { fetches.push(fetch('/admin/count/count-data-until?date=' + date) .then(response => response.json()) .then(response => { - let total_requests = response.data.total - localStorage.setItem("until-" + date, total_requests); - element.innerHTML = total_requests - element.classList.remove("loading"); + let total_requests = parseInt(response.data.total) + if (!data[days_ago]) { + data[days_ago] = {} + } + data[days_ago]["same_time"] = total_requests; }) .catch(reason => { - element.innerHTML = "-" - element.classList.remove("loading"); + if (!data[days_ago]) { + data[days_ago] = {} + } + data[days_ago]["same_time"] = 0; })); } else { break; diff --git a/metager/resources/less/metager/pages/count/style.less b/metager/resources/less/metager/pages/count/style.less index 8bacf9659d0604ab2639e6191c4470a8af1dcf7a..9e78e1ac5425543ca3c7337be8c183248896b7de 100644 --- a/metager/resources/less/metager/pages/count/style.less +++ b/metager/resources/less/metager/pages/count/style.less @@ -3,12 +3,23 @@ td { text-align: center; } -table.table-striped > tbody > tr:nth-child(odd) { +td.date { + text-align: left; +} + +table.table-striped>tbody>tr:nth-child(odd) { background-color: lightgrey; } -table td.loading { +.loading { background-image: url("/img/ajax-loader.gif"); background-repeat: no-repeat; background-position: center; +} + +.total-median .loading, +.record .loading { + width: 30px; + height: 30px; + display: inline-block; } \ No newline at end of file diff --git a/metager/resources/views/admin/count.blade.php b/metager/resources/views/admin/count.blade.php index 2af7476be9a1cc6accfe5f06b674c33ef9f57fea..ff528b5149e514723094474e8afe3aa5254bbd51 100644 --- a/metager/resources/views/admin/count.blade.php +++ b/metager/resources/views/admin/count.blade.php @@ -3,59 +3,57 @@ @section('title', $title ) @section('content') - <div id="graph"> - - </div> - <p>Am ??? zur gleichen Zeit <span class="text-info">???</span> - insgesamt <span class="text-danger">???</span></p> - <p>Mittelwert der letzten ??? Tage: ???</p> - <table class="table table-striped"> - <caption> - <form method="GET" style="display: flex; align-items: center;"> - <div class="form-group" style="max-width: 100px; margin-right: 8px;"> - <label for="days">Tage</label> - <input class="form-control" type="number" id="days" name="days" value="{{$days}}" /> - </div> - <div class="form-group" style="max-width: 100px; margin-right: 8px;"> - <label for="interface">Sprache</label> - <select class="form-control" name="interface" id="interface"> - <option value="all" {{ (Request::input('interface', 'all') == "all" ? "selected" : "")}}>Alle</option> - <option value="de" {{ (Request::input('interface', 'all') == "de" ? "selected" : "")}}>DE</option> - <option value="en" {{ (Request::input('interface', 'all') == "en" ? "selected" : "")}}>EN</option> - <option value="es" {{ (Request::input('interface', 'all') == "es" ? "selected" : "")}}>ES</option> - </select> - </div> - <div id="refresh" style="margin-top: 11px; margin-right: 8px;"> - <button type="submit" class="btn btn-sm btn-default">Aktualisieren</button> - </div> - <div id="export" style="margin-top: 11px;"> - <button type="submit" name="out" value="csv" class="btn btn-sm btn-default">Als CSV exportieren</button> - </div> - </form> - </caption> - <thead> - <tr> - <th>Datum</th> - <th>Suchanfragen zur gleichen Zeit</th> - <th>Suchanfragen insgesamt</th> - <th>Mittelwert (bis zum jeweiligen Tag zurück)</th> - </tr> - </thead> - <tbody> - <tr class="today"> - <td class="date" data-date="{{ (new DateTime('midnight'))->format("Y-m-d") }}">{{ (new DateTime('midnight'))->format("d.m.Y") }}</td> - <td class="loading same-time"></td> - <td class="total">-</td> - <td class="median">-</td> - </tr> - @for($i = 1; $i < $days; $i++) - <tr @if($i === 0)class="today loading"@else class="loading"@endif> - <td class="date" data-date="{{ (new DateTime('midnight'))->modify("-" . $i . " days")->format("Y-m-d") }}">{{ (new DateTime('midnight'))->modify("-" . $i . " days")->format("d.m.Y") }}</td> - <td class="loading same-time"></td> - <td class="loading total"></td> - <td class="loading median"></td> +<div id="graph"> + <svg> + + </svg> +</div> +<p class="record">Am <span class="record-date loading"></span> zur gleichen Zeit <span class="record-same-time text-info loading"></span> - insgesamt <span class="record-total text-danger loading"></span></p> +<p class="total-median">Mittelwert der letzten <span class="median-days loading"></span> Tage: <span class="median-value loading"></span></p> +<table class="table table-striped"> + <caption> + <form method="GET" style="display: flex; align-items: center;"> + <div class="form-group" style="max-width: 100px; margin-right: 8px;"> + <label for="days">Tage</label> + <input class="form-control" type="number" id="days" name="days" value="{{$days}}" /> + </div> + <div class="form-group" style="max-width: 100px; margin-right: 8px;"> + <label for="interface">Sprache</label> + <select class="form-control" name="interface" id="interface"> + <option value="all" {{ (Request::input('interface', 'all') == "all" ? "selected" : "")}}>Alle</option> + <option value="de" {{ (Request::input('interface', 'all') == "de" ? "selected" : "")}}>DE</option> + <option value="en" {{ (Request::input('interface', 'all') == "en" ? "selected" : "")}}>EN</option> + <option value="es" {{ (Request::input('interface', 'all') == "es" ? "selected" : "")}}>ES</option> + </select> + </div> + <div id="refresh" style="margin-top: 11px; margin-right: 8px;"> + <button type="submit" class="btn btn-sm btn-default">Aktualisieren</button> + </div> + <div id="export" style="margin-top: 11px;"> + <button type="submit" name="out" value="csv" class="btn btn-sm btn-default">Als CSV exportieren</button> + </div> + </form> + </caption> + <thead> + <tr> + <th>Datum</th> + <th>Suchanfragen zur gleichen Zeit</th> + <th>Suchanfragen insgesamt</th> + <th>Mittelwert (bis zum jeweiligen Tag zurück)</th> + </tr> + </thead> + <tbody> + @for($i = 0; $i < $days; $i++) <tr class="{{ $i % 7 === 0 ? 'same-day' : ''}}" data-days_ago="{{$i}}"> + @php + $date = (new Carbon())->subDays($i); + @endphp + <td class="date" data-date="{{ $date->format("Y-m-d") }}" data-date_formatted="{{ $date->format("d.m.Y")}}">{{ (new Carbon())->locale("de_DE")->subDays($i)->translatedFormat("d.m.Y - l") }}</td> + <td class="loading same-time"></td> + <td class="loading total"></td> + <td class="loading median"></td> </tr> @endfor - </tbody> - </table> + </tbody> +</table> -@endsection +@endsection \ No newline at end of file