summaryrefslogtreecommitdiffstats
path: root/modules-available/statistics
diff options
context:
space:
mode:
authorJannik Schönartz2018-02-06 13:45:54 +0100
committerJannik Schönartz2018-02-06 13:45:54 +0100
commit3d801dcde21c8166e3281a180ef21ff05b175fd6 (patch)
treef8508e11bc6df5a27f0417ad8098c4ed013a691c /modules-available/statistics
parent[usb-lock-off] Reworked config chooser. Switched from the dropdown config sel... (diff)
parent[statistics_reporting] Translation for 'settings' (diff)
downloadslx-admin-3d801dcde21c8166e3281a180ef21ff05b175fd6.tar.gz
slx-admin-3d801dcde21c8166e3281a180ef21ff05b175fd6.tar.xz
slx-admin-3d801dcde21c8166e3281a180ef21ff05b175fd6.zip
Merge branch 'origin/master' into usb-lock-off
Diffstat (limited to 'modules-available/statistics')
-rw-r--r--modules-available/statistics/inc/filter.inc.php44
-rw-r--r--modules-available/statistics/lang/de/messages.json6
-rw-r--r--modules-available/statistics/lang/de/permissions.json5
-rw-r--r--modules-available/statistics/lang/de/template-tags.json7
-rw-r--r--modules-available/statistics/lang/en/messages.json6
-rw-r--r--modules-available/statistics/lang/en/permissions.json5
-rw-r--r--modules-available/statistics/lang/en/template-tags.json7
-rw-r--r--modules-available/statistics/page.inc.php101
-rw-r--r--modules-available/statistics/pages/replace.inc.php119
-rw-r--r--modules-available/statistics/permissions/permissions.json5
-rw-r--r--modules-available/statistics/templates/clientlist.html62
-rw-r--r--modules-available/statistics/templates/filterbox.html8
-rw-r--r--modules-available/statistics/templates/machine-notes.html4
-rw-r--r--modules-available/statistics/templates/page-replace.html4
14 files changed, 330 insertions, 53 deletions
diff --git a/modules-available/statistics/inc/filter.inc.php b/modules-available/statistics/inc/filter.inc.php
index be6df752..f6765059 100644
--- a/modules-available/statistics/inc/filter.inc.php
+++ b/modules-available/statistics/inc/filter.inc.php
@@ -88,15 +88,17 @@ class Filter
$lhs = trim(substr($q, 0, $pos));
$rhs = trim(substr($q, $pos + strlen($operator)));
- if ($lhs == 'gbram') {
+ if ($lhs === 'gbram') {
$filters[] = new RamGbFilter($operator, $rhs);
- } elseif ($lhs == 'state') {
+ } elseif ($lhs === 'runtime') {
+ $filters[] = new RuntimeFilter($operator, $rhs);
+ } elseif ($lhs === 'state') {
$filters[] = new StateFilter($operator, $rhs);
- } elseif ($lhs == 'hddgb') {
+ } elseif ($lhs === 'hddgb') {
$filters[] = new Id44Filter($operator, $rhs);
- } elseif ($lhs == 'location') {
+ } elseif ($lhs === 'location') {
$filters[] = new LocationFilter($operator, $rhs);
- } elseif ($lhs == 'subnet') {
+ } elseif ($lhs === 'subnet') {
$filters[] = new SubnetFilter($operator, $rhs);
} else {
if (array_key_exists($lhs, Page_Statistics::$columns) && Page_Statistics::$columns[$lhs]['column']) {
@@ -143,6 +145,38 @@ class RamGbFilter extends Filter
}
}
+class RuntimeFilter extends Filter
+{
+ public function __construct($operator, $argument)
+ {
+ parent::__construct('lastboot', $operator, $argument);
+ }
+
+ public function whereClause(&$args, &$joins)
+ {
+ global $SIZE_RAM;
+ $upper = time() - (int)$this->argument * 3600;
+ $lower = $upper - 3600;
+ $common = "state IN ('OCCUPIED', 'IDLE', 'STANDBY') AND";
+ if ($this->operator == '=') {
+ return "$common ({$this->column} BETWEEN $lower AND $upper)";
+ } elseif ($this->operator == '<') {
+ return "$common {$this->column} > $upper";
+ } elseif ($this->operator == '<=') {
+ return "$common {$this->column} > $lower";
+ } elseif ($this->operator == '>') {
+ return "$common {$this->column} < $lower";
+ } elseif ($this->operator == '>=') {
+ return "$common {$this->column} < $upper";
+ } elseif ($this->operator == '!=') {
+ return "$common ({$this->column} < $lower OR {$this->column} > $upper)";
+ } else {
+ error_log("unimplemented operator in RuntimeFilter: $this->operator");
+ return ' 1';
+ }
+ }
+}
+
class Id44Filter extends Filter
{
public function __construct($operator, $argument)
diff --git a/modules-available/statistics/lang/de/messages.json b/modules-available/statistics/lang/de/messages.json
index c9667f7b..a9256d5a 100644
--- a/modules-available/statistics/lang/de/messages.json
+++ b/modules-available/statistics/lang/de/messages.json
@@ -1,7 +1,11 @@
{
"deleted-n-machines": "{{0}} Clients gel\u00f6scht",
+ "ignored-both-in-use": "Rechnerpaar ignoriert, da beide noch in Betrieb zu sein scheinen. ({{0}} und {{1}})",
"invalid-filter-argument": "Das Argument {{1}} ist nicht g\u00fcltig f\u00fcr den Filter {{0}}",
"invalid-filter-key": "{{0}} ist kein g\u00fcltiges Filterkriterium",
+ "invalid-replace-format": "Ung\u00fcltiges Parameterformat ({{0}})",
+ "no-replacement-matches": "Keine Rechner gefunden, die den oben genannten Kriterien entsprechen",
"notes-saved": "Anmerkungen gespeichert",
- "unknown-machine": "Unbekannte Rechner-ID {{0}}"
+ "unknown-machine": "Unbekannte Rechner-ID {{0}}",
+ "x-machines-replaced": "{{0}} Rechner ersetzt"
} \ No newline at end of file
diff --git a/modules-available/statistics/lang/de/permissions.json b/modules-available/statistics/lang/de/permissions.json
new file mode 100644
index 00000000..15303993
--- /dev/null
+++ b/modules-available/statistics/lang/de/permissions.json
@@ -0,0 +1,5 @@
+{
+ "view": "Client Statistiken anschauen.",
+ "note": "Client Notizen speichern.",
+ "delete": "Client löschen."
+} \ No newline at end of file
diff --git a/modules-available/statistics/lang/de/template-tags.json b/modules-available/statistics/lang/de/template-tags.json
index 3cdde813..84c4690c 100644
--- a/modules-available/statistics/lang/de/template-tags.json
+++ b/modules-available/statistics/lang/de/template-tags.json
@@ -66,8 +66,14 @@
"lang_ramSlots": "Speicher-Slots",
"lang_realCores": "Kerne",
"lang_reallocatedSectors": "Defekte Sektoren",
+ "lang_replace": "Ersetzen",
+ "lang_replaceInstructions": "Hier k\u00f6nnen Sie Metadaten automatisch \u00fcbertragen, wenn in einem Raum die Rechner ausgetauscht wurden. Dies setzt voraus, dass alle neuen Rechner die gleiche IP Adresse erhalten haben wie der Rechner, der zuvor am entsprechenden Platz stand, und die neuen Rechner alle einmal gestartet wurden. In der Liste unten sehen Sie alle Rechnerpaare, auf die folgendes zutrifft: 1) Die IP-Adressen sind identisch 2) Der letzte Boot des einen Rechners liegt vor dem ersten Boot des anderen Rechners. W\u00e4hlen Sie alle Rechnerpaare aus, f\u00fcr die eine Ersetzung stattfinden soll. Bei der Ersetzung werden alle Logeintr\u00e4ge, Sitzungslogs, Position im Raumplan und evtl. spezielle Betriebsmodi vom alten Rechner auf den neuen \u00dcbertragen.",
+ "lang_replaceMachinesHeading": "Rechner ersetzen",
+ "lang_replaceNew": "Alter Rechner",
+ "lang_replaceOld": "Neuer Rechner",
"lang_runMode": "Betriebsmodus",
"lang_runmodeMachines": "Mit besonderem Betriebsmodus",
+ "lang_runtimeHours": "Laufzeit (Stunden)",
"lang_screens": "Bildschirme",
"lang_serialNo": "Serien-Nr",
"lang_showList": "Liste",
@@ -75,6 +81,7 @@
"lang_sockets": "Sockel",
"lang_subnet": "Subnetz",
"lang_sureDeletePermanent": "M\u00f6chten Sie diese(n) Rechner wirklich unwiderruflich aus der Datenbank entfernen?\r\n\r\nWichtig: L\u00f6schen verhindert nicht, dass ein Rechner nach erneutem Starten von bwLehrpool wieder in die Datenbank aufgenommen wird.",
+ "lang_sureReplaceNoUndo": "Wollen Sie die Daten ausgew\u00e4hlten Rechner \u00fcbertragen? Diese Aktion kann nicht r\u00fcckg\u00e4ngig gemacht werden.",
"lang_tempPart": "Temp. Partition",
"lang_tempPartStats": "Tempor\u00e4re Partition",
"lang_thoseAreProjectors": "Diese Modellnamen werden als Beamer behandelt, auch wenn die EDID-Informationen des Ger\u00e4tes anderes berichten.",
diff --git a/modules-available/statistics/lang/en/messages.json b/modules-available/statistics/lang/en/messages.json
index 3471c472..0f290f2e 100644
--- a/modules-available/statistics/lang/en/messages.json
+++ b/modules-available/statistics/lang/en/messages.json
@@ -1,7 +1,11 @@
{
"deleted-n-machines": "Deleted {{0}} clients",
+ "ignored-both-in-use": "Ignoring machine pair as both still seem to be in use. ({{0}} and {{1}})",
"invalid-filter-argument": "{{1}} is not a vald argument for filter {{0}}",
"invalid-filter-key": "{{0}} is not a valid filter",
+ "invalid-replace-format": "Invalid parameter format ({{0}})",
+ "no-replacement-matches": "No machines match the criteria from above",
"notes-saved": "Notes have been saved",
- "unknown-machine": "Unknown machine uuid {{0}}"
+ "unknown-machine": "Unknown machine uuid {{0}}",
+ "x-machines-replaced": "{{0}} machine(s) replaced"
} \ No newline at end of file
diff --git a/modules-available/statistics/lang/en/permissions.json b/modules-available/statistics/lang/en/permissions.json
new file mode 100644
index 00000000..7be32f22
--- /dev/null
+++ b/modules-available/statistics/lang/en/permissions.json
@@ -0,0 +1,5 @@
+{
+ "view": "View client statistics.",
+ "note": "Save client notes.",
+ "delete": "Delete client."
+} \ No newline at end of file
diff --git a/modules-available/statistics/lang/en/template-tags.json b/modules-available/statistics/lang/en/template-tags.json
index 35c4e68a..b064ee50 100644
--- a/modules-available/statistics/lang/en/template-tags.json
+++ b/modules-available/statistics/lang/en/template-tags.json
@@ -66,8 +66,14 @@
"lang_ramSlots": "Memory slots",
"lang_realCores": "Cores",
"lang_reallocatedSectors": "Bad sectors",
+ "lang_replace": "Replace",
+ "lang_replaceInstructions": "If some PCs\/clients have been physically replaced, you can re-assign log entries, session data, position information etc. from the old machine to the new one. This requires that the new machine gets assigned the same IP address as the old one and, if the room planner is used -- that it is placed in the same spot as the old one. The list below shows all machine pairs where 1) the last boot of one machine lies before the first boot of the other one 2) both machines had the same IP address last time they booted. The replacement action will reassign all log events, room plan location and special run mode from the old machine to the new machine.",
+ "lang_replaceMachinesHeading": "Replace machines",
+ "lang_replaceNew": "Old machine",
+ "lang_replaceOld": "New machine",
"lang_runMode": "Mode of operation",
"lang_runmodeMachines": "With special mode of operation",
+ "lang_runtimeHours": "Runtime (hours)",
"lang_screens": "Screens",
"lang_serialNo": "Serial no",
"lang_showList": "List",
@@ -75,6 +81,7 @@
"lang_sockets": "Sockets",
"lang_subnet": "Subnet",
"lang_sureDeletePermanent": "Are your sure you want to delete the selected machine(s) from the database? This cannot be undone.\r\n\r\nNote: Deleting machines from the database does not prevent booting up bwLehrpool again, which would recreate their respective database entries.",
+ "lang_sureReplaceNoUndo": "Are you sure you want to replace the selected machine pairs? This action cannot be undone.",
"lang_tempPart": "Temp. partition",
"lang_tempPartStats": "Temporary partition",
"lang_thoseAreProjectors": "These model names will always be treated as beamers, even if the device's EDID data says otherwise.",
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index ea5b6f03..5fe4ebfa 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -21,6 +21,8 @@ class Page_Statistics extends Page
private $query;
+ private $locationsAllowedToView;
+
/**
* @var bool whether we have a SubPage from the pages/ subdir
*/
@@ -102,6 +104,11 @@ class Page_Statistics extends Page
'type' => 'string',
'column' => true
],
+ 'hostname' => [
+ 'op' => Page_Statistics::$op_stringcmp,
+ 'type' => 'string',
+ 'column' => true
+ ],
'subnet' => [
'op' => Page_Statistics::$op_nominal,
'type' => 'string',
@@ -117,7 +124,12 @@ class Page_Statistics extends Page
'type' => 'enum',
'column' => true,
'values' => ['occupied', 'on', 'off', 'idle', 'standby']
- ]
+ ],
+ 'runtime' => [
+ 'op' => Page_Statistics::$op_ordinal,
+ 'type' => 'int',
+ 'column' => true
+ ],
];
if (Module::isAvailable('locations')) {
Page_Statistics::$columns['location'] = [
@@ -134,11 +146,14 @@ class Page_Statistics extends Page
{
$this->initConstants();
User::load();
- if (!User::hasPermission('superadmin')) {
+ if (!User::isLoggedIn()) {
Message::addError('main.no-permission');
Util::redirect('?do=Main');
}
+ $this->locationsAllowedToView = User::getAllowedLocations("view");
+
+
$show = Request::any('show', 'stat', 'string');
$show = preg_replace('/[^a-z0-9_\-]/', '', $show);
@@ -153,16 +168,20 @@ class Page_Statistics extends Page
$action = Request::post('action');
if ($action === 'setnotes') {
$uuid = Request::post('uuid', '', 'string');
- $text = Request::post('content', '', 'string');
- if (empty($text)) {
- $text = null;
+ $locationid = Database::queryFirst('SELECT locationid FROM machine WHERE machineuuid = :uuid',
+ array('uuid' => $uuid))['locationid'];
+ if (User::hasPermission("note", $locationid)) {
+ $text = Request::post('content', '', 'string');
+ if (empty($text)) {
+ $text = null;
+ }
+ Database::exec('UPDATE machine SET notes = :text WHERE machineuuid = :uuid', array(
+ 'uuid' => $uuid,
+ 'text' => $text,
+ ));
+ Message::addSuccess('notes-saved');
+ Util::redirect('?do=Statistics&uuid=' . $uuid);
}
- Database::exec('UPDATE machine SET notes = :text WHERE machineuuid = :uuid', array(
- 'uuid' => $uuid,
- 'text' => $text,
- ));
- Message::addSuccess('notes-saved');
- Util::redirect('?do=Statistics&uuid=' . $uuid);
} elseif ($action === 'delmachines') {
$this->deleteMachines();
Util::redirect('?do=statistics', true);
@@ -188,10 +207,12 @@ class Page_Statistics extends Page
$res = Database::simpleQuery('SELECT machineuuid, locationid FROM machine WHERE machineuuid IN (:ids)', compact('ids'));
$ids = array_flip($ids);
$delete = [];
+ $allowedLocations = User::getAllowedLocations("delete");
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- // TODO: Check locationid permissions
- unset($ids[$row['machineuuid']]);
- $delete[] = $row['machineuuid'];
+ if (in_array($row['locationid'], $allowedLocations)) {
+ unset($ids[$row['machineuuid']]);
+ $delete[] = $row['machineuuid'];
+ }
}
if (!empty($delete)) {
Database::exec('DELETE FROM machine WHERE machineuuid IN (:delete)', compact('delete'));
@@ -277,7 +298,8 @@ class Page_Statistics extends Page
foreach (Location::getLocations() as $loc) {
$locsFlat['L' . $loc['locationid']] = array(
'pad' => $loc['locationpad'],
- 'name' => $loc['locationname']
+ 'name' => $loc['locationname'],
+ 'disabled' => !in_array($loc['locationid'], $this->locationsAllowedToView)
);
}
}
@@ -333,8 +355,9 @@ class Page_Statistics extends Page
private function showSummary($filterSet)
{
$filterSet->makeFragments($where, $join, $sort, $args);
-
- $known = Database::queryFirst("SELECT Count(*) AS val FROM machine $join WHERE ($where)", $args);
+ $args['allowedLocations'] = $this->locationsAllowedToView;
+ $where = "locationid IN (:allowedLocations) AND ($where)";
+ $known = Database::queryFirst("SELECT Count(*) AS val FROM machine $join WHERE $where", $args);
// If we only have one machine, redirect to machine details
if ($known['val'] == 1) {
$this->redirectFirst($where, $join, $args);
@@ -394,7 +417,8 @@ class Page_Statistics extends Page
global $STATS_COLORS;
$filterSet->makeFragments($where, $join, $sort, $args);
-
+ $args['allowedLocations'] = $this->locationsAllowedToView;
+ $where = "locationid IN (:allowedLocations) AND ($where)";
$res = Database::simpleQuery('SELECT systemmodel, Round(AVG(realcores)) AS cores, Count(*) AS `count` FROM machine'
. " $join WHERE $where GROUP BY systemmodel ORDER BY `count` DESC, systemmodel ASC", $args);
$lines = array();
@@ -427,7 +451,8 @@ class Page_Statistics extends Page
global $STATS_COLORS, $SIZE_RAM;
$filterSet->makeFragments($where, $join, $sort, $args);
-
+ $args['allowedLocations'] = $this->locationsAllowedToView;
+ $where = "locationid IN (:allowedLocations) AND ($where)";
$res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine $join WHERE $where GROUP BY mbram", $args);
$lines = array();
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
@@ -473,7 +498,8 @@ class Page_Statistics extends Page
private function showKvmState($filterSet)
{
$filterSet->makeFragments($where, $join, $sort, $args);
-
+ $args['allowedLocations'] = $this->locationsAllowedToView;
+ $where = "locationid IN (:allowedLocations) AND ($where)";
$colors = array('UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6');
$res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine $join WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args);
$lines = array();
@@ -497,7 +523,8 @@ class Page_Statistics extends Page
global $STATS_COLORS, $SIZE_ID44;
$filterSet->makeFragments($where, $join, $sort, $args);
-
+ $args['allowedLocations'] = $this->locationsAllowedToView;
+ $where = "locationid IN (:allowedLocations) AND ($where)";
$res = Database::simpleQuery("SELECT id44mb, Count(*) AS `count` FROM machine $join WHERE $where GROUP BY id44mb", $args);
$lines = array();
$total = 0;
@@ -549,7 +576,8 @@ class Page_Statistics extends Page
private function showLatestMachines($filterSet)
{
$filterSet->makeFragments($where, $join, $sort, $args);
-
+ $args['allowedLocations'] = $this->locationsAllowedToView;
+ $where = "locationid IN (:allowedLocations) AND ($where)";
$args['cutoff'] = ceil(time() / 3600) * 3600 - 86400 * 10;
$res = Database::simpleQuery("SELECT machineuuid, clientip, hostname, firstseen, mbram, kvmstate, id44mb FROM machine $join"
@@ -583,7 +611,8 @@ class Page_Statistics extends Page
{
Module::isAvailable('js_stupidtable');
$filterSet->makeFragments($where, $join, $sort, $args);
-
+ $args['allowedLocations'] = $this->locationsAllowedToView;
+ $where = "locationid IN (:allowedLocations) AND ($where)";
$xtra = '';
if ($filterSet->isNoId44Filter()) {
$xtra .= ', data';
@@ -594,18 +623,20 @@ class Page_Statistics extends Page
$join .= ' LEFT JOIN runmode USING (machineuuid) ';
}
}
- $res = Database::simpleQuery('SELECT machineuuid, macaddr, clientip, lastseen,'
+ $res = Database::simpleQuery('SELECT machineuuid, locationid, macaddr, clientip, lastseen,'
. ' logintime, state, realcores, mbram, kvmstate, cpumodel, id44mb, hostname, notes IS NOT NULL AS hasnotes,'
. ' badsectors ' . $xtra . ' FROM machine'
. " $join WHERE $where $sort", $args);
$rows = array();
$singleMachine = 'none';
+ $deleteAllowedLocations = User::getAllowedLocations("delete");
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
if ($singleMachine === 'none') {
$singleMachine = $row['machineuuid'];
} else {
$singleMachine = false;
}
+ $row['deleteAllowed'] = in_array($row['locationid'], $deleteAllowedLocations);
$row['state_' . $row['state']] = true;
//$row['firstseen'] = Util::prettyTime($row['firstseen']);
$row['lastseen_int'] = $row['lastseen'];
@@ -630,6 +661,13 @@ class Page_Statistics extends Page
}
}
$row['cpumodel'] = preg_replace('/\(R\)|\(TM\)|\bintel\b|\bamd\b|\bcpu\b|dual-core|\bdual\s+core\b|\bdual\b|\bprocessor\b/i', ' ', $row['cpumodel']);
+ if (!empty($row['rmmodule'])) {
+ $data = RunMode::getRunMode($row['machineuuid'], RunMode::DATA_STRINGS);
+ if ($data !== false) {
+ $row['moduleName'] = $data['moduleName'];
+ $row['modeName'] = $data['modeName'];
+ }
+ }
$rows[] = $row;
}
if ($singleMachine !== false && $singleMachine !== 'none') {
@@ -749,6 +787,10 @@ class Page_Statistics extends Page
Message::addError('unknown-machine', $uuid);
return;
}
+ if (!in_array($client['locationid'], $this->locationsAllowedToView)) {
+ Message::addError('main.no-permission');
+ return;
+ }
// Hack: Get raw collected data
if (Request::get('raw', false)) {
Header('Content-Type: text/plain; charset=utf-8');
@@ -912,9 +954,9 @@ class Page_Statistics extends Page
// Not seen in last two weeks
$spans['graph'] .= '<div style="background:#444;left:0;width:100%">&nbsp;</div>';
}
- if (isset($client['state_occupied'])) {
+ if ($client['state'] === 'OCCUPIED') {
$spans['graph'] .= '<div style="background:#e99;left:' . round(($client['logintime'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['logintime'] + 900) * $scale, 2) . '%">&nbsp;</div>';
- } elseif (isset($client['state_off'])) {
+ } elseif ($client['state'] === 'OFFLINE') {
$spans['graph'] .= '<div style="background:#444;left:' . round(($client['lastseen'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['lastseen'] + 900) * $scale, 2) . '%">&nbsp;</div>';
}
$t = explode('-', date('Y-n-j-G', $cutoff));
@@ -960,6 +1002,7 @@ class Page_Statistics extends Page
));
}
// Notes
+ $client["notesAllowed"] = User::hasPermission("note", $client["locationid"]);
Render::addTemplate('machine-notes', $client);
}
@@ -1026,7 +1069,11 @@ class Page_Statistics extends Page
public static function getPciId($cat, $id)
{
- return Database::queryFirst('SELECT value, dateline FROM pciid WHERE category = :cat AND id = :id LIMIT 1',
+ static $cache = [];
+ $key = $cat . '-' . $id;
+ if (isset($cache[$key]))
+ return $cache[$key];
+ return $cache[$key] = Database::queryFirst('SELECT value, dateline FROM pciid WHERE category = :cat AND id = :id LIMIT 1',
array('cat' => $cat, 'id' => $id));
}
diff --git a/modules-available/statistics/pages/replace.inc.php b/modules-available/statistics/pages/replace.inc.php
new file mode 100644
index 00000000..ae9c6108
--- /dev/null
+++ b/modules-available/statistics/pages/replace.inc.php
@@ -0,0 +1,119 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+ $action = Request::post('action', false, 'string');
+ if ($action === 'replace') {
+ self::handleReplace();
+ }
+ if (Request::isPost()) {
+ Util::redirect('?do=statistics&show=replace');
+ }
+ }
+
+ private static function handleReplace()
+ {
+ $replace = Request::post('replace', false, 'array');
+ if ($replace === false || empty($replace)) {
+ Message::addError('main.parameter-empty', 'replace');
+ return;
+ }
+ $list = [];
+ foreach ($replace as $p) {
+ $split = explode('x', $p);
+ if (count($split) !== 2) {
+ Message::addError('invalid-replace-format', $p);
+ continue;
+ }
+ $entry = ['old' => $split[0], 'new' => $split[1]];
+ $old = Database::queryFirst('SELECT lastseen FROM machine WHERE machineuuid = :old',
+ ['old' => $entry['old']]);
+ if ($old === false) {
+ Message::addError('unknown-machine', $entry['old']);
+ continue;
+ }
+ $new = Database::queryFirst('SELECT firstseen FROM machine WHERE machineuuid = :new',
+ ['new' => $entry['new']]);
+ if ($new === false) {
+ Message::addError('unknown-machine', $entry['new']);
+ continue;
+ }
+ if ($old['lastseen'] - 86400*7 > $new['firstseen']) {
+ Message::addWarning('ignored-both-in-use', $entry['old'], $entry['new']);
+ continue;
+ }
+ $entry['datelimit'] = min($new['firstseen'], $old['lastseen']);
+ $list[] = $entry;
+ }
+ if (empty($list)) {
+ Message::addError('main.parameter-empty', 'replace');
+ return;
+ }
+
+ // First handle module internal tables
+ foreach ($list as $entry) {
+ Database::exec('UPDATE statistic SET machineuuid = :new WHERE machineuuid = :old AND dateline < :datelimit', $entry);
+ }
+
+ // Let other modules do their thing
+ $fun = function($file, $list) {
+ include $file;
+ };
+ foreach (Hook::load('statistics/machine-replace') as $hook) {
+ $fun($hook->file, $list);
+ }
+
+ // Finalize by updating machine table
+ foreach ($list as $entry) {
+ unset($entry['datelimit']);
+ Database::exec('UPDATE machine old, machine new SET
+ new.fixedlocationid = old.fixedlocationid,
+ new.position = old.position,
+ old.position = NULL,
+ new.notes = old.notes,
+ old.notes = NULL,
+ old.lastseen = new.firstseen
+ WHERE old.machineuuid = :old AND new.machineuuid = :new', $entry);
+ }
+ Message::addSuccess('x-machines-replaced', count($list));
+ }
+
+ public static function doRender()
+ {
+ self::listSuggestions();
+ }
+
+ private static function listSuggestions()
+ {
+ if (Request::get('debug', false) !== false) {
+ $oldCutoff = time() - 86400 * 180;
+ $newCutoff = time() - 86400 * 180;
+ } else {
+ $oldCutoff = time() - 86400 * 90;
+ $newCutoff = time() - 86400 * 30;
+ }
+ $res = Database::simpleQuery("SELECT
+ old.machineuuid AS olduuid, old.locationid AS oldlid, old.hostname AS oldhost,
+ old.clientip AS oldip, old.macaddr AS oldmac, old.lastseen AS oldlastseen, old.systemmodel AS oldmodel,
+ new.machineuuid AS newuuid, new.locationid AS newlid, new.hostname AS newhost,
+ new.clientip AS newip, new.macaddr AS newmac, new.firstseen AS newfirstseen, new.systemmodel AS newmodel
+ FROM machine old INNER JOIN machine new ON (old.clientip = new.clientip AND old.lastseen < new.firstseen AND old.lastseen > $oldCutoff AND new.firstseen > $newCutoff)
+ ORDER BY oldhost ASC, oldip ASC");
+ $list = [];
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $row['oldlastseen_s'] = Util::prettyTime($row['oldlastseen']);
+ $row['newfirstseen_s'] = Util::prettyTime($row['newfirstseen']);
+ $list[] = $row;
+ }
+ $data = array('pairs' => $list);
+ Render::addTemplate('page-replace', $data);
+ if (empty($list)) {
+ Message::addInfo('no-replacement-matches');
+ }
+ }
+
+}
+
diff --git a/modules-available/statistics/permissions/permissions.json b/modules-available/statistics/permissions/permissions.json
new file mode 100644
index 00000000..97a49036
--- /dev/null
+++ b/modules-available/statistics/permissions/permissions.json
@@ -0,0 +1,5 @@
+[
+ "view",
+ "note",
+ "delete"
+] \ No newline at end of file
diff --git a/modules-available/statistics/templates/clientlist.html b/modules-available/statistics/templates/clientlist.html
index 13e148fa..d06eb4f7 100644
--- a/modules-available/statistics/templates/clientlist.html
+++ b/modules-available/statistics/templates/clientlist.html
@@ -10,28 +10,28 @@
<td></td>
<td></td>
<td class="text-right">
- <button class="btn btn-default btn-xs" onclick="popupFilter('lastseen')">
- <span id="btn_filter_lastseen" class="glyphicon glyphicon-filter"></span>
+ <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('lastseen')">
+ <span class="glyphicon glyphicon-filter"></span>
</button>
</td>
<td>
- <button class="btn btn-default btn-xs" onclick="popupFilter('kvmstate')">
- <span id="btn_filter_kvmstate" class="glyphicon glyphicon-filter"></span>
+ <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('kvmstate')">
+ <span class="glyphicon glyphicon-filter"></span>
</button>
</td>
<td class="text-right">
- <button class="btn btn-default btn-xs" onclick="popupFilter('gbram')">
- <span id="btn_filter_gbram" class="glyphicon glyphicon-filter"></span>
+ <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('gbram')">
+ <span class="glyphicon glyphicon-filter"></span>
</button>
</td>
<td class="text-right">
- <button class="btn btn-default btn-xs" onclick="popupFilter('hddgb')">
- <span id="btn_filter_hddgb" class="glyphicon glyphicon-filter"></span>
+ <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('hddgb')">
+ <span class="glyphicon glyphicon-filter"></span>
</button>
</td>
<td>
- <button class="btn btn-default btn-xs" onclick="popupFilter('realcores')">
- <span id="btn_filter_cpu" class="glyphicon glyphicon-filter"></span>
+ <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('realcores')">
+ <span class="glyphicon glyphicon-filter"></span>
</button>
</td>
</tr>
@@ -49,11 +49,15 @@
{{#rows}}
<tr>
<td data-sort-value="{{hostname}}" class="text-nowrap">
+ {{#deleteAllowed}}
<div class="checkbox checkbox-inline">
- <input type="checkbox" name="uuid[]" value="{{machineuuid}}">
+ <input type="checkbox" name="uuid[]" value="{{machineuuid}}" class="deleteCheckboxes">
<label></label>
</div>
- {{#hasnotes}}<span class="glyphicon glyphicon-exclamation-sign pull-right"></span>{{/hasnotes}}
+ {{/deleteAllowed}}
+ {{#hasnotes}}
+ <span class="glyphicon glyphicon-exclamation-sign pull-right"></span>
+ {{/hasnotes}}
{{#state_OFFLINE}}
<span class="glyphicon glyphicon-off" title="{{lang_machineOff}}"></span>
{{/state_OFFLINE}}
@@ -68,7 +72,11 @@
{{/state_STANDBY}}
<a href="?do=Statistics&amp;uuid={{machineuuid}}"><b>{{hostname}}</b></a>
<div class="small">{{machineuuid}}</div>
- {{#rmmodule}}<div class="small">{{lang_runMode}}: <a class="slx-bold" href="?do=runmode&amp;module={{rmmodule}}">{{rmmodule}}</a></div>{{/rmmodule}}
+ {{#rmmodule}}
+ <div class="small">{{lang_runMode}}:
+ <a class="slx-bold" href="?do=runmode&amp;module={{rmmodule}}">{{moduleName}}</a> / {{modeName}}
+ </div>
+ {{/rmmodule}}
</td>
<td data-sort-value="{{clientip}}"><b><a href="?do=Statistics&amp;show=list&amp;filters=subnet={{subnet}}">{{subnet}}</a>{{lastoctet}}</b><br>{{macaddr}}</td>
<td data-sort-value="{{lastseen_int}}" class="text-right text-nowrap">{{lastseen}}</td>
@@ -91,10 +99,10 @@
</table>
<div class="text-right buttonbar">
<button type="reset" class="btn btn-default">
- <span class="glyphicon glyphicon-remove"></span>
+ <span class="glyphicon glyphicon-refresh"></span>
{{lang_reset}}
</button>
- <button type="button" class="btn btn-danger" onclick="$('#del-confirm').modal()">
+ <button id="deleteButton" type="button" class="btn btn-danger" onclick="$('#del-confirm').modal()">
<span class="glyphicon glyphicon-trash"></span>
{{lang_delete}}
</button>
@@ -122,6 +130,9 @@
</form>
<script type="application/javascript"><!--
+
+selectedMachineCounter = 0;
+
document.addEventListener("DOMContentLoaded", function () {
['gbram', 'hddgb', 'realcores', 'kvmstate', 'lastseen', 'clientip'].forEach(function (v) {
var $sortBtn = $('#sortButton-' + v);
@@ -138,6 +149,27 @@ document.addEventListener("DOMContentLoaded", function () {
container: 'body',
trigger : 'hover'
});
+
+ $("#deleteButton").prop("disabled", true);
+ $(".deleteCheckboxes").change(function() {
+ if ($(this).is(':checked')) {
+ selectedMachineCounter++;
+ if (selectedMachineCounter === 1) {
+ $("#deleteButton").prop("disabled", false);
+ }
+ } else {
+ selectedMachineCounter--;
+ if (selectedMachineCounter === 0) {
+ $("#deleteButton").prop("disabled", true);
+ }
+ }
+ });
+
+ $("button[type=reset]").click(function() {
+ selectedMachineCounter = 0;
+ $("#deleteButton").prop("disabled", true);
+ });
+
});
function toggleButton(v) {
diff --git a/modules-available/statistics/templates/filterbox.html b/modules-available/statistics/templates/filterbox.html
index 32464031..58b66a75 100644
--- a/modules-available/statistics/templates/filterbox.html
+++ b/modules-available/statistics/templates/filterbox.html
@@ -99,7 +99,9 @@ var slxFilterNames = {
state: '{{lang_usageState}}',
location: '{{lang_location}}',
currentuser: '{{lang_currentUser}}',
- subnet: '{{lang_subnet}}'
+ subnet: '{{lang_subnet}}',
+ runtime: '{{lang_runtimeHours}}',
+ hostname: '{{lang_hostname}}'
};
slxLocations = {{{locations}}};
@@ -176,10 +178,12 @@ document.addEventListener("DOMContentLoaded", function () {
$('#argumentSelect').show();
columns[col]['values'].forEach(function (v) {
var t = v;
+ var disabled = true;
if (col === 'location' && slxLocations['L' + v]) {
t = slxLocations['L' + v].pad + ' ' + slxLocations['L' + v].name;
+ disabled = slxLocations['L' + v].disabled;
}
- $('#argumentSelect').append($('<option>', { value: v, text: t }));
+ $('#argumentSelect').append($('<option>', { value: v, text: t, disabled: disabled }));
});
} else {
$('#argumentInput').datepicker('remove');
diff --git a/modules-available/statistics/templates/machine-notes.html b/modules-available/statistics/templates/machine-notes.html
index 22ed96e9..66e44da4 100644
--- a/modules-available/statistics/templates/machine-notes.html
+++ b/modules-available/statistics/templates/machine-notes.html
@@ -8,9 +8,9 @@
<input type="hidden" name="token" value="{{token}}">
<input type="hidden" name="action" value="setnotes">
<input type="hidden" name="uuid" value="{{machineuuid}}">
- <textarea name="content" class="form-control" cols="101" rows="10">{{notes}}</textarea>
+ <textarea name="content" class="form-control" cols="101" rows="10" {{^notesAllowed}}disabled{{/notesAllowed}}>{{notes}}</textarea>
<br/>
- <button type="submit" class="btn btn-primary pull-right"><span class="glyphicon glyphicon-floppy-disk"></span> {{lang_save}}</button>
+ <button type="submit" class="btn btn-primary pull-right" {{^notesAllowed}}disabled{{/notesAllowed}}><span class="glyphicon glyphicon-floppy-disk"></span> {{lang_save}}</button>
</form>
</div>
</div>
diff --git a/modules-available/statistics/templates/page-replace.html b/modules-available/statistics/templates/page-replace.html
index f87610a2..d0e9f766 100644
--- a/modules-available/statistics/templates/page-replace.html
+++ b/modules-available/statistics/templates/page-replace.html
@@ -17,6 +17,10 @@
}
</style>
+<p>
+ {{lang_replaceInstructions}}
+</p>
+
<form method="post" action="?do=statistics&amp;show=replace">
<input type="hidden" name="token" value="{{token}}">
<table class="reptable">