diff --git a/CHANGELOG.md b/CHANGELOG.md
index 88dbb0d..9a14c06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
ChangeLog
--- develop ---
+* feature: Add a Fleet (Summary) Dashboard reached by a dashboard glyph on the Summary tab's Device Type rows: a draggable, add/remove card grid scoped to all devices or one OS type, with min/avg/max bullet charts (CPU/memory/swap/processes/uptime), a device-status stacked bar, OS type distribution, storage utilization, fleet stat tiles, and top processes by CPU and by memory; scope, card order, expanded state and refresh interval persist per user
* issue: Fix 'Uncaught TypeError: runCollector(): Argument #2 ($lastrun) must be of type float, null given' in the poller when a collector's *_lastrun/*_freq setting has never been written (e.g. a task's first run); runCollector() now accepts null and normalizes it
* issue: Fix the per-device Dashboard Storage, Per-Processor Load, Hardware and Installed Software cards rendering empty because they filtered on 'present = 1' while the underlying Storage/Hardware/Inventory tabs never did, and add a Process Use History card
* feature: Add a per-device Dashboard that drills into a single host with a draggable, add/remove card grid (CPU/memory/swap gauges, per-processor load, storage, top processes, hardware, installed software, logins and system information); card order, expanded state, the selected device and the auto-refresh interval persist per user, and a dashboard glyph on the Devices tab opens a stateful Dashboard tab that remembers the last host opened
diff --git a/css/hmib.css b/css/hmib.css
index ebc4a41..2e61951 100644
--- a/css/hmib.css
+++ b/css/hmib.css
@@ -496,3 +496,204 @@
grid-column: 1 / -1;
}
}
+
+/*
+ * Fleet (Summary) Dashboard visuals: min/avg/max bullet charts, stat tiles, a
+ * status stacked bar and in-table mini bars. Theme-adaptive via
+ * color-mix(currentColor ...); severity accents use fixed colours.
+ */
+
+/* Min / avg / max bullet chart. The track is 0..scale; a translucent band spans
+ min..max; a strong marker sits at avg. */
+.hmibBullet {
+ margin: 9px 0;
+}
+
+.hmibBulletHead {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 8px;
+ margin-bottom: 4px;
+}
+
+.hmibBulletLabel {
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.hmibBulletAvg {
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+ color: color-mix(in srgb, currentColor 78%, transparent);
+}
+
+.hmibBulletTrack {
+ position: relative;
+ height: 16px;
+ border-radius: 8px;
+ background: color-mix(in srgb, currentColor 12%, transparent);
+ overflow: hidden;
+}
+
+.hmibBulletRange {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ border-radius: 8px;
+}
+
+.hmibBulletRange--up { background: color-mix(in srgb, #3c9a5f 55%, transparent); }
+.hmibBulletRange--recovering { background: color-mix(in srgb, #d9830f 55%, transparent); }
+.hmibBulletRange--down { background: color-mix(in srgb, #cc0000 55%, transparent); }
+.hmibBulletRange--none { background: color-mix(in srgb, currentColor 32%, transparent); }
+
+.hmibBulletMarker {
+ position: absolute;
+ top: -2px;
+ bottom: -2px;
+ width: 3px;
+ transform: translateX(-1px);
+ background: currentColor;
+ border-radius: 2px;
+}
+
+.hmibBulletScale {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 2px;
+ font-size: 10px;
+ font-variant-numeric: tabular-nums;
+ color: color-mix(in srgb, currentColor 60%, transparent);
+}
+
+/* Stat tiles for the fleet overview. */
+.hmibFleetTiles {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
+ gap: 8px;
+}
+
+.hmibFleetTile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2px;
+ padding: 8px 6px;
+ border-radius: 7px;
+ border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
+ background: color-mix(in srgb, currentColor 4%, transparent);
+}
+
+.hmibFleetTileValue {
+ font-size: 22px;
+ font-weight: 700;
+ line-height: 1;
+ font-variant-numeric: tabular-nums;
+}
+
+.hmibFleetTileLabel {
+ font-size: 11px;
+ text-align: center;
+ color: color-mix(in srgb, currentColor 70%, transparent);
+}
+
+.hmibFleetTile--up { border-color: color-mix(in srgb, #3c9a5f 55%, transparent); }
+.hmibFleetTile--rec { border-color: color-mix(in srgb, #d9830f 55%, transparent); }
+.hmibFleetTile--down { border-color: color-mix(in srgb, #cc0000 55%, transparent); }
+.hmibFleetTile--up .hmibFleetTileValue { color: #3c9a5f; }
+.hmibFleetTile--rec .hmibFleetTileValue { color: #d9830f; }
+.hmibFleetTile--down .hmibFleetTileValue { color: #cc0000; }
+
+/* Horizontal status stacked bar (up / recovering / down / disabled). */
+.hmibStackBar {
+ display: flex;
+ height: 20px;
+ border-radius: 6px;
+ overflow: hidden;
+ background: color-mix(in srgb, currentColor 12%, transparent);
+}
+
+.hmibStackSeg {
+ height: 100%;
+}
+
+.hmibStackSeg--up { background: #3c9a5f; }
+.hmibStackSeg--rec { background: #d9830f; }
+.hmibStackSeg--down { background: #cc0000; }
+.hmibStackSeg--dis { background: color-mix(in srgb, currentColor 35%, transparent); }
+
+.hmibStackLegend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-top: 8px;
+ font-size: 11px;
+}
+
+.hmibStackLegend span {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.hmibStackChip {
+ width: 10px;
+ height: 10px;
+ border-radius: 2px;
+ display: inline-block;
+}
+
+/* Horizontal distribution bars (OS type counts) and in-table mini bars. */
+.hmibDistRow {
+ display: grid;
+ grid-template-columns: minmax(80px, 34%) 1fr auto;
+ align-items: center;
+ gap: 8px;
+ margin: 6px 0;
+ font-size: 12px;
+}
+
+.hmibDistLabel {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.hmibDistTrack {
+ height: 12px;
+ border-radius: 6px;
+ background: color-mix(in srgb, currentColor 12%, transparent);
+ overflow: hidden;
+}
+
+.hmibDistFill {
+ display: block;
+ height: 100%;
+ border-radius: 6px;
+ background: color-mix(in srgb, currentColor 45%, transparent);
+}
+
+.hmibDistValue {
+ font-variant-numeric: tabular-nums;
+ color: color-mix(in srgb, currentColor 80%, transparent);
+}
+
+.hmibMiniBar {
+ display: block;
+ position: relative;
+ height: 10px;
+ min-width: 60px;
+ border-radius: 5px;
+ background: color-mix(in srgb, currentColor 12%, transparent);
+ overflow: hidden;
+}
+
+.hmibMiniBarFill {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ border-radius: 5px;
+ background: color-mix(in srgb, currentColor 45%, transparent);
+}
diff --git a/hmib.php b/hmib.php
index c49cf8a..81cb03a 100644
--- a/hmib.php
+++ b/hmib.php
@@ -81,6 +81,24 @@
exit;
}
+if (get_request_var('action') == 'summary_card') {
+ header('Content-Type: application/json; charset=UTF-8');
+ print hmib_summary_card_ajax();
+ exit;
+}
+
+if (get_request_var('action') == 'summary_layout') {
+ header('Content-Type: application/json; charset=UTF-8');
+ print hmib_summary_layout_save();
+ exit;
+}
+
+if (get_request_var('action') == 'summary_refresh') {
+ header('Content-Type: application/json; charset=UTF-8');
+ print hmib_summary_refresh_save();
+ exit;
+}
+
general_header();
// Load this plugin's per-theme glyph styling: base rules plus the active
@@ -126,6 +144,10 @@
case 'dashboard':
hmib_dashboard();
+ break;
+ case 'summary_dashboard':
+ hmib_summary_dashboard();
+
break;
case 'history':
hmib_history();
@@ -2914,14 +2936,27 @@ function hmib_tabs(): void {
// set the default tab
$current_tab = get_request_var('action');
- // The stateful per-device Dashboard tab only appears once a device has been
- // opened from the Devices tab (it remembers the last device viewed).
- if ($current_tab == 'dashboard' || (int) read_user_setting('hmib_dashboard_host', 0) > 0) {
- $tabs = array_slice($tabs, 0, 2, true)
- + ['dashboard' => __esc('Dashboard', 'hmib')]
- + array_slice($tabs, 2, null, true);
+ // The stateful Dashboard tabs only appear once opened from their glyph: the
+ // Fleet Dashboard from the Summary tab, the per-device Dashboard from Devices.
+ $show_fleet = ($current_tab == 'summary_dashboard' || read_user_setting('hmib_summary_type', '') !== '');
+ $show_device = ($current_tab == 'dashboard' || (int) read_user_setting('hmib_dashboard_host', 0) > 0);
+
+ $ordered = [];
+
+ foreach ($tabs as $tab_key => $tab_label) {
+ $ordered[$tab_key] = $tab_label;
+
+ if ($tab_key == 'summary' && $show_fleet) {
+ $ordered['summary_dashboard'] = __esc('Fleet Dashboard', 'hmib');
+ }
+
+ if ($tab_key == 'devices' && $show_device) {
+ $ordered['dashboard'] = __esc('Dashboard', 'hmib');
+ }
}
+ $tabs = $ordered;
+
// draw the tabs
print "