<?php
class SubPage
{
private static $STATS_COLORS;
public static function doPreprocess()
{
User::assertPermission('view.summary');
if (!Module::isAvailable('js_chart')) {
ErrorHandler::traceError('js_chart not available');
}
}
public static function doRender()
{
$filters = StatisticsFilter::parseQuery();
$filterSet = new StatisticsFilterSet($filters);
if (!$filterSet->setAllowedLocationsFromPermission('view.summary')) {
Message::addError('main.no-permission');
Util::redirect('?do=main');
}
// Prepare chart colors
self::$STATS_COLORS = [];
for ($i = 0; $i < 10; ++$i) {
self::$STATS_COLORS[] = '#55' . sprintf('%02s%02s', dechex(
(int)((($i + 1) * ($i + 1)) / .3922)),
dechex((int)(abs((5 - $i) * 51))));
}
if (!$filterSet->hasSpecificFilters()) {
$filterSet->filterNonClients();
}
StatisticsFilter::renderFilterBox('summary', $filterSet);
Render::openTag('div', array('class' => 'row'));
self::showSummary($filterSet);
self::showMemory($filterSet);
self::showId44($filterSet);
self::showKvmState($filterSet);
self::showLatestMachines($filterSet);
self::showSystemModels($filterSet);
Render::closeTag('div');
}
private static function showSummary(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$known = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE $where", $args);
$on = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE state IN ('IDLE', 'OCCUPIED') AND ($where)", $args);
$used = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE state = 'OCCUPIED' AND ($where)", $args);
$hdd = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE badsectors >= 10 AND ($where)", $args);
if ($on['val'] != 0) {
$usedpercent = round($used['val'] / $on['val'] * 100);
} else {
$usedpercent = 0;
}
$data = [
'known' => $known['val'],
'online' => $on['val'],
'used' => $used['val'],
'usedpercent' => $usedpercent,
'badhdd' => $hdd['val'],
];
// Graph
$labels = [];
$points1 = [];
$points2 = [];
$lectures = [];
// Get locations
if ($filterSet->suitableForUsageGraph()) {
$locFilter = $filterSet->hasFilter('LocationStatisticsFilter');
if ($locFilter === null
|| ($locFilter->op === '~' && ($locFilter->argument == 0
|| (is_array($locFilter->argument) && in_array(0, $locFilter->argument))))) {
$locations = null;
$op = null;
} elseif ($locFilter->op === '~') {
$locations = array_keys(Location::getRecursiveFlat($locFilter->argument));
$op = $locFilter->op;
} else {
if (is_array($locFilter->argument)) {
$locations = $locFilter->argument;
} else {
$locations = [$locFilter->argument];
}
$op = $locFilter->op;
}
//error_log($op . ' ' . print_r($locations, true));
$cutoff = time() - 2 * 86400;
$res = Database::simpleQuery("SELECT dateline, data FROM statistic
WHERE typeid = '~stats' AND dateline > $cutoff ORDER BY dateline DESC");
// Get max from 4 consecutive values, which should be 4*5 = 20m
$sum = 0;
foreach ($res as $row) {
if ($row['data'][0] === '{') {
$x = json_decode($row['data'], true);
if (!is_array($x) || !isset($x['usage']))
continue;
$x = self::mangleStatsJson($x, $locations, $op);
} else if ($locations === null) {
$x = explode('#', $row['data']);
if (count($x) < 3)
continue;
$x[] = 0;
} else {
continue;
}
if ($sum % 4 === 0) {
$labels[] = date('H:i', $row['dateline']);
} else {
$x[1] = max($x[1], array_pop($points1));
$x[2] = max($x[2], array_pop($points2));
$x[3] += array_pop($lectures);
}
$points1[] = $x[1];
$points2[] = $x[2];
$lectures[] = $x[3];
++$sum;
}
}
if (!empty($points1) && max($points1) > 0) {
$labels = array_reverse($labels);
$points1 = array_reverse($points1);
$points2 = array_reverse($points2);
$lectures = array_reverse($lectures);
$data['json'] = json_encode(['labels' => $labels,
'datasets' => [
['data' => $points1, 'label' => 'Online', 'borderColor' => '#8f3'],
['data' => $points2, 'label' => 'In use', 'borderColor' => '#e76'],
]]);
$data['markings'] = json_encode($lectures);
}
if (Module::get('runmode') !== false) {
$res = Database::queryFirst('SELECT Count(*) AS cnt FROM machine m INNER JOIN runmode r USING (machineuuid)'
. " $join WHERE $where", $args);
$data['runmode'] = $res['cnt'];
}
// Draw
Render::addTemplate('summary', $data);
}
private static function showSystemModels(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$res = Database::simpleQuery('SELECT systemmodel, Round(AVG(realcores)) AS cores, Count(*) AS `count` FROM machine m'
. " $join WHERE $where GROUP BY systemmodel ORDER BY `count` DESC, systemmodel ASC", $args);
$lines = [];
$json = [];
$id = 0;
foreach ($res as $row) {
if (empty($row['systemmodel'])) {
continue;
}
settype($row['count'], 'integer');
$row['urlsystemmodel'] = urlencode($row['systemmodel']);
$row['idx'] = count($lines);
$lines[] = $row;
$json[] = [
'color' => self::$STATS_COLORS[$id % count(self::$STATS_COLORS)],
'value' => $row['count'],
];
++$id;
}
self::capChart($json, $lines, 0.92);
Render::addTemplate('cpumodels', ['rows' => $lines, 'json' => json_encode($json)]);
}
private static function alignBySteps(int $value, array $steps): int
{
for ($i = 1; $i < count($steps); ++$i) {
if ($steps[$i] < $value) {
continue;
}
if ($steps[$i] - $value >= $value - $steps[$i - 1]) {
--$i;
}
return $steps[$i];
}
return $value;
}
private static function showMemory(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine m $join
WHERE $where GROUP BY mbram", $args);
$lines = [];
foreach ($res as $row) {
$gb = self::alignBySteps((int)ceil($row['mbram'] / 1024), StatisticsFilter::SIZE_RAM);
$lines[$gb] = ($lines[$gb] ?? 0) + $row['count'];
}
asort($lines);
$data = ['rows' => []];
$json = [];
$id = 0;
foreach (array_reverse($lines, true) as $k => $v) {
$data['rows'][] = [
'idx' => count($data['rows']),
'gb' => $k,
'count' => $v,
'class' => StatisticsStyling::ramColorClass($k * 1024),
];
$json[] = [
'color' => self::$STATS_COLORS[$id % count(self::$STATS_COLORS)],
'value' => $v,
];
++$id;
}
self::capChart($json, $data['rows'], 0.92);
$data['json'] = json_encode($json);
Render::addTemplate('memory', $data);
}
private static function showKvmState(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$colors = ['UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6'];
$res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine m $join
WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args);
$lines = [];
$json = [];
foreach ($res as $row) {
$row['idx'] = count($lines);
$lines[] = $row;
$json[] = array(
'color' => $colors[$row['kvmstate']] ?? '#000',
'value' => $row['count'],
);
}
Render::addTemplate('kvmstate', array('rows' => $lines, 'json' => json_encode($json)));
}
private static function showId44(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$res = Database::simpleQuery("SELECT id44mb, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY id44mb", $args);
$lines = array();
$total = 0;
foreach ($res as $row) {
$total += $row['count'];
$gb = self::alignBySteps((int)ceil($row['id44mb'] / 1024), StatisticsFilter::SIZE_PARTITION);
$lines[$gb] = ($lines[$gb] ?? 0) + $row['count'];
}
asort($lines);
$data = array('rows' => array());
$json = array();
$id = 0;
foreach (array_reverse($lines, true) as $k => $v) {
$data['rows'][] = [
'idx' => count($data['rows']),
'gb' => $k,
'count' => $v,
'class' => StatisticsStyling::hddColorClass($k),
];
if ($k === 0) {
$color = '#e55';
} else {
$color = self::$STATS_COLORS[$id++ % count(self::$STATS_COLORS)];
}
$json[] = array(
'color' => $color,
'value' => $v,
);
}
self::capChart($json, $data['rows'], 0.95);
$data['json'] = json_encode($json);
Render::addTemplate('id44', $data);
}
private static function showLatestMachines(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$args['cutoff'] = ceil(time() / 3600) * 3600 - 86400 * 10;
$res = Database::simpleQuery("SELECT m.machineuuid, m.clientip, m.hostname, m.firstseen, m.mbram, m.kvmstate, m.id44mb
FROM machine m $join
WHERE firstseen > :cutoff AND $where
ORDER BY firstseen DESC LIMIT 32", $args);
$rows = array();
$count = 0;
foreach ($res as $row) {
if (empty($row['hostname'])) {
$row['hostname'] = $row['clientip'];
}
$row['firstseen_int'] = $row['firstseen'];
$row['firstseen'] = Util::prettyTime($row['firstseen']);
$row['gbram'] = round(round($row['mbram'] / 500) / 2, 1); // Trial and error until we got "expected" rounding..
$row['gbtmp'] = round($row['id44mb'] / 1024);
$row['ramclass'] = StatisticsStyling::ramColorClass((int)$row['mbram']);
$row['kvmclass'] = StatisticsStyling::kvmColorClass($row['kvmstate']);
$row['hddclass'] = StatisticsStyling::hddColorClass((int)$row['gbtmp']);
$row['kvmicon'] = $row['kvmstate'] === 'ENABLED' ? '✓' : '✗';
if (++$count > 5) {
$row['collapse'] = 'collapse';
}
$rows[] = $row;
}
Render::addTemplate('newclients', array('rows' => $rows, 'openbutton' => $count > 5));
}
/*
* HELPERS
*/
private static function capChart(array &$json, array &$rows, float $cutoff, float $minSlice = 0.015): void
{
$total = 0;
foreach ($json as $entry) {
$total += $entry['value'];
}
if ($total === 0) {
return;
}
$cap = ceil($total * $cutoff);
$accounted = 0;
$id = 0;
foreach ($json as $entry) {
if (($accounted >= $cap || $entry['value'] / $total < $minSlice) && $id >= 3) {
break;
}
++$id;
$accounted += $entry['value'];
}
for ($i = $id; $i < count($rows); ++$i) {
$rows[$i]['collapse'] = 'collapse';
}
$json = array_slice($json, 0, $id);
if ($accounted / $total < 0.99) {
$json[] = array(
'color' => '#eee',
'label' => 'invalid',
'value' => ($total - $accounted),
);
}
}
/**
* @param array $json decoded json ~stats data
* @param ?int[] $locations
*/
private static function mangleStatsJson(array $json, ?array $locations, ?string $op): array
{
// Total, On, InUse, Lectures
$retval = [0, 0, 0, 0];
foreach ($json['usage'] as $lid => $data) {
$lid = (int)$lid;
if ($locations === null
|| ($op === '!=' && !in_array($lid, $locations))
|| ($op !== '!=' && in_array($lid, $locations))) {
$retval[0] += $data['t'];
$retval[1] += $data['o'] ?? 0;
$retval[2] += $data['u'] ?? 0;
if (isset($data['event'])) {
$retval[3] += 1;
}
}
}
return $retval;
}
}