lots of php proofreading and html tab tidying up
This commit is contained in:
1 parent
46d9f86d55
commit
8437fa6d9c
25 files changed
+247
-624
No files matched your search
@@ -1,4 +0,0 @@
|
||||
# This is the class definition for the remote Storage Systems
|
||||
# There will be a StorageLinux and StorageWindows Class, as well as the LocalServer Class
|
||||
# The LocalServer class will mostly be a List of Storage server objects and Class functions for interacting with them
|
||||
# The actual Storage server Classes will mostly just be collections of variables
|
||||
@@ -1,6 +0,0 @@
|
||||
### This file contains the flask routes for interfacing with the DriveServer objects
|
||||
### I need routes for adding/updating windows/linux hosts, as well as a query route
|
||||
### There needs to also be a Redis handler, meaning also that this will render with PHP
|
||||
### but have javascript to update if any Redis data happens
|
||||
### This won't happen a lot, but it will happen occasionally
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"notes:": "this is both a scratch file and a reference for new component descriptors"
|
||||
"notes:": "this is both a scratch file and a reference for new component descriptors",
|
||||
"descriptor descriptor": "the reference descriptor is the next key"
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -12,7 +12,6 @@ from requests import RequestException, Response
|
||||
# import needed Class Libraries
|
||||
from Storage import *
|
||||
from Helpers import *
|
||||
#SummaryServer = DriveHealthServer(get_hostname())
|
||||
|
||||
SummaryServer = load_state()
|
||||
|
||||
@@ -20,12 +19,12 @@ if SummaryServer is None:
|
||||
SummaryServer = DriveHealthServer(get_hostname())
|
||||
print("Created new SummaryServer")
|
||||
|
||||
# declare flask apps
|
||||
# declare flask app
|
||||
app = Flask(__name__)
|
||||
#scheduler = APScheduler()
|
||||
|
||||
|
||||
############################
|
||||
# Flask routes
|
||||
############################
|
||||
|
||||
# client update
|
||||
@app.route('/storage_client_update', methods=['POST'])
|
||||
@@ -49,7 +48,6 @@ def storage_client_delete():
|
||||
print(result)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
# client details
|
||||
@app.route('/client_details', methods=['GET'])
|
||||
def client_details():
|
||||
@@ -86,20 +84,15 @@ def test_route():
|
||||
"DriveHealthServer": f"{SummaryServer}"
|
||||
})
|
||||
|
||||
|
||||
# test route 2
|
||||
@app.route('/test_storage_summary', methods=['GET'])
|
||||
def test_storage_summary():
|
||||
return jsonify({
|
||||
"message": "Hello world!",
|
||||
"hostname": get_hostname(),
|
||||
"DriveHealthServer": f"{SummaryServer}"
|
||||
})
|
||||
|
||||
|
||||
return test_route()
|
||||
|
||||
|
||||
############################
|
||||
# Route Helpers
|
||||
############################
|
||||
|
||||
# helper function for client_update route
|
||||
# handles the submission data from the flask route
|
||||
@@ -112,6 +105,12 @@ def client_update_helper(payload: dict):
|
||||
result = client_processor(processed_payload)
|
||||
return result
|
||||
|
||||
# client processing function, add/update logic in Class Methods
|
||||
def client_processor(client_dict: dict):
|
||||
result = SummaryServer.process_client_data(client_dict)
|
||||
save_state(SummaryServer)
|
||||
return result
|
||||
|
||||
# handle submission from remove route
|
||||
def client_remove_helper(payload: dict):
|
||||
result = None
|
||||
@@ -147,30 +146,12 @@ def post_processor(client_dict: dict, required_keys: dict):
|
||||
client_dict["processed_at"] = time.time()
|
||||
return client_dict
|
||||
|
||||
# Main functions
|
||||
|
||||
# client processing function, add/update logic in Class Methods
|
||||
def client_processor(client_dict: dict):
|
||||
result = SummaryServer.process_client_data(client_dict)
|
||||
save_state(SummaryServer)
|
||||
return result
|
||||
|
||||
def background_loop():
|
||||
return True
|
||||
############################
|
||||
# Main function
|
||||
############################
|
||||
|
||||
def run_main():
|
||||
#if SummaryServer is none:
|
||||
|
||||
#atexit.register(lambda: save_state(SummaryServer)) test
|
||||
# Flask scheduler for background loop, run if requested
|
||||
#scheduler.add_job(id='background_loop',
|
||||
# func=background_loop,
|
||||
# trigger='interval',
|
||||
# seconds=60)
|
||||
#scheduler.init_app(app)
|
||||
#scheduler.start()
|
||||
|
||||
# Flask API
|
||||
background_loop()
|
||||
app.run(debug=False, host='0.0.0.0', port=5001)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,5 @@
|
||||
# main function for storage API
|
||||
# this is the barest of bones to load the api
|
||||
|
||||
# import class libraries
|
||||
from Routes import *
|
||||
|
||||
Binary file not shown.
@@ -9,7 +9,7 @@ date_default_timezone_set('America/Los_Angeles');
|
||||
$remove_hosts = [];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
if ($_POST['action'] === 'remove') {
|
||||
// The Remove form sends a comma‑separated string of short_id’s
|
||||
// The Remove form sends a comma-separated string of short_id’s
|
||||
if (!empty($_POST['remove_hosts'])) {
|
||||
$remove_hosts = array_filter(
|
||||
explode(',', $_POST['remove_hosts']),
|
||||
@@ -156,7 +156,7 @@ if (!in_array($mode, $validModes, true)) {
|
||||
$apiConfig = [
|
||||
'cosmostat' => ['bind' => '10.200.27.20', 'port' => '5000'],
|
||||
/*'gali' => ['bind' => '10.200.27.20', 'port' => '5000'], // same as cosmostat*/
|
||||
'drive_health' => ['bind' => '0.0.0.0', 'port' => '5001'], // new API
|
||||
'drive_health' => ['bind' => '172.25.1.18', 'port' => '5001'], // new API
|
||||
];
|
||||
|
||||
/* ---------- Helper: fetch client details ---------- */
|
||||
@@ -237,9 +237,6 @@ $systemComponents = $properties['system_components'] ?? [];
|
||||
$selectedHost = $clients[$selectedIdx]['hostname'] ?? 'Unknown';
|
||||
|
||||
|
||||
|
||||
/* ---- ---- */
|
||||
|
||||
/* ---- Sidebar Renderer ---- */
|
||||
|
||||
function renderSidebar(string $mode){
|
||||
@@ -255,74 +252,54 @@ function renderSidebar(string $mode){
|
||||
<nav class="sidebar">
|
||||
<form method="get" id="modeForm">
|
||||
<label for="modeSelect">Mode:</label>
|
||||
<select name="mode" id="modeSelect" onchange="this.form.submit()">
|
||||
<select class="select-dark" name="mode" id="modeSelect" onchange="this.form.submit()">
|
||||
<?php foreach ($modes as $key => $label): ?><option value="<?= h($key) ?>" <?= $mode === $key ? 'selected' : '' ?>><?= h($label) ?></option>
|
||||
<?php endforeach; ?></select>
|
||||
</form>
|
||||
<p>
|
||||
|
||||
<?php if ($mode === 'drive_health'): ?>
|
||||
|
||||
<?php
|
||||
if ( !is_array($clients) || empty($clients) ) {
|
||||
// Graceful “no data” handling
|
||||
echo '<p style="margin:1rem 0; font-style:italic;">'
|
||||
. 'No hosts are available to manage at this time.'
|
||||
. '</p>';
|
||||
|
||||
<?php
|
||||
if ( !is_array($clients) || empty($clients) ) {
|
||||
// Graceful “no data” handling
|
||||
echo '<p style="margin:1rem 0; font-style:italic;">'
|
||||
. 'No hosts are available to manage at this time.'
|
||||
. '</p>';
|
||||
}
|
||||
?>
|
||||
<?php if (is_array($clients) && !empty($clients)): ?>
|
||||
<h3>Hosts</h3>
|
||||
<form method="get" id="driveHealthForm">
|
||||
<input type="hidden" name="mode" value="drive_health">
|
||||
<ul>
|
||||
<?php foreach ($clients as $c): ?>
|
||||
<?php $id = $c['short_id']; ?>
|
||||
<li>
|
||||
<label>
|
||||
<input type="checkbox" name="hosts[]" value="<?= h($id) ?>"
|
||||
<?= in_array($id, $selectedHosts, true) ? 'checked' : '' ?>>
|
||||
<?= h($c['name']) ?>
|
||||
</label>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul><p>
|
||||
<button type="submit" name="action" value="apply">Apply</button><p>
|
||||
<button type="submit" name="action" value="all">Select All</button><br>
|
||||
<button type="submit" name="action" value="none">Select None</button><p>
|
||||
</form>
|
||||
<!-- Remove host button (POST) -->
|
||||
<form method="post" id="removeForm">
|
||||
<input type="hidden" name="mode" value="drive_health">
|
||||
<input type="hidden" name="action" value="remove">
|
||||
<input type="hidden" name="remove_hosts" id="remove_hosts_input" value="">
|
||||
<button type="submit">Remove</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
?>
|
||||
<?php if (is_array($clients) && !empty($clients)): ?>
|
||||
|
||||
<?php if ($mode == 'cosmostat'): ?>
|
||||
|
||||
<input type="hidden" name="host" value="<?= h($selectedId) ?>">
|
||||
<h3>Endpoints</h3>
|
||||
<ol id="endpointList"></ol>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($mode == 'gali'): ?>
|
||||
|
||||
<h3>Shuttle Gali</h3>
|
||||
<?php endif; ?>
|
||||
<!--
|
||||
<?php if (!empty($remove_hosts)): ?>
|
||||
<h4>Remove Selected:</h4>
|
||||
<h3>Hosts</h3>
|
||||
<form method="get" id="driveHealthForm">
|
||||
<input type="hidden" name="mode" value="drive_health">
|
||||
<ul>
|
||||
<?php foreach ($remove_hosts as $sid): ?>
|
||||
<li><?= h($sid) ?></li>
|
||||
<?php foreach ($clients as $c): ?>
|
||||
<?php $id = $c['short_id']; ?>
|
||||
|
||||
<li>
|
||||
<label>
|
||||
<input type="checkbox" name="hosts[]" value="<?= h($id) ?>" <?= in_array($id, $selectedHosts, true) ? 'checked' : '' ?>><?= h($c['name']) ?>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</ul><p>
|
||||
<button class="btn btn-primary" type="submit" name="action" value="apply">Apply</button><p>
|
||||
<button class="btn btn-primary" type="submit" name="action" value="all">Select All</button><p>
|
||||
<button class="btn btn-primary" type="submit" name="action" value="none">Select None</button><p>
|
||||
</form>
|
||||
<!-- Remove host button (POST) -->
|
||||
<form method="post" id="removeForm">
|
||||
<input type="hidden" name="mode" value="drive_health">
|
||||
<input type="hidden" name="action" value="remove">
|
||||
<input type="hidden" name="remove_hosts" id="remove_hosts_input" value="">
|
||||
<button class="btn btn-primary" type="submit">Remove</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
-->
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($mode == 'gali'): ?>
|
||||
<h3>Shuttle Gali</h3>
|
||||
<?php endif; ?>
|
||||
|
||||
</nav>
|
||||
|
||||
@@ -344,7 +321,8 @@ function renderMainContent(string $mode){
|
||||
echo '<div class="card"><p>No hosts selected.</p></div>';
|
||||
return;
|
||||
}
|
||||
echo '<div class="storage_client">';
|
||||
echo '
|
||||
<div class="storage_client">';
|
||||
foreach ($selectedHosts as $sid) {
|
||||
// Find the client that matches this short_id
|
||||
$c = null;
|
||||
@@ -357,41 +335,49 @@ function renderMainContent(string $mode){
|
||||
if ($c === null) continue; // safety
|
||||
|
||||
$hostname = $c['name'] ?? 'Unknown';
|
||||
echo '<div class="card">
|
||||
';
|
||||
echo '<h2>Drive Health - ' . h($hostname) . '</h2>
|
||||
<h3>IP: '.h($c['ip']).'<br>
|
||||
Timestamp: '.date('F j, Y g:i a', (int) $c['timestamp']).' </h3>
|
||||
';
|
||||
echo '
|
||||
<div class="card">
|
||||
<h2>Drive Health - ' . h($hostname) . '</h2>
|
||||
<h3>IP: '.h($c['ip']).'<br>
|
||||
Timestamp: '.date('F j, Y g:i a', (int) $c['timestamp']).' </h3>
|
||||
';
|
||||
|
||||
if (isset($c['drives']) && is_array($c['drives']) && count($c['drives']) > 0) {
|
||||
echo '<table id="host_metrics_table">';
|
||||
echo '<thead><tr><th>Drive Letter</th><th>Disk ID</th><th>Health Status</th><th>Model</th><th>Capacity</th><th>Power On Hours</th><th>Host Writes</th><th>Wear Level</th></tr></thead>
|
||||
';
|
||||
echo '<tbody>
|
||||
';
|
||||
echo '
|
||||
<table id="host_metrics_table">
|
||||
<thead><tr>
|
||||
<th>Drive Letter</th><th>Disk ID</th><th>Health Metrics</th><th>Model</th><th>Serial</th><th>Capacity</th>
|
||||
</tr></thead>
|
||||
<tbody>';
|
||||
foreach ($c['drives'] as $drive) {
|
||||
echo '<tr>
|
||||
';
|
||||
echo '<td>' . h($drive['drive_letter'] ?? '') . '</td>';
|
||||
echo '<td>' . h("".$drive['disk_id'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['health_status'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['model'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['capacity'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['power_on_hours'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['host_writes'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['wear_level'] ?? '') . '</td>';
|
||||
echo '</tr>
|
||||
';
|
||||
$wear_gb = '';
|
||||
if (isset($drive['host_writes'])){
|
||||
$wear_gb = $drive['host_writes'].' wear';
|
||||
}
|
||||
echo '
|
||||
<tr>
|
||||
<td>' . h($drive['drive_letter'] ?? '') . '</td>
|
||||
<td>' . h("".$drive['disk_id'] ?? '') . '</td>
|
||||
<td>' .
|
||||
h($drive['health_status'] ?? '').'<br>'.
|
||||
h($drive['power_on_hours'] ?? '').'<br>'.
|
||||
h($wear_gb ?? '').
|
||||
'</td>
|
||||
<td>' . h($drive['model'] ?? '') . '</td>
|
||||
<td>' . h($drive['serial'] ?? '') . '</td>
|
||||
<td>' . h($drive['capacity'] ?? '') . '</td>
|
||||
</tr>';
|
||||
}
|
||||
echo '</tbody></table>
|
||||
';
|
||||
echo '
|
||||
</tbody></table> ';
|
||||
} else {
|
||||
echo '<p>No drive data available for this host.</p>';
|
||||
}
|
||||
echo '</div>';
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
echo '
|
||||
</div>';
|
||||
return;
|
||||
}
|
||||
?>
|
||||
@@ -477,10 +463,12 @@ function renderMainContent(string $mode){
|
||||
<body>
|
||||
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php renderSidebar($mode); ?>
|
||||
<!-- Main content -->
|
||||
<?php renderMainContent($mode); ?>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<?php renderSidebar($mode); ?>
|
||||
|
||||
<!-- Main content -->
|
||||
<?php renderMainContent($mode); ?>
|
||||
|
||||
|
||||
</div> <!-- /wrapper -->
|
||||
@@ -507,11 +495,11 @@ function renderMainContent(string $mode){
|
||||
<script>
|
||||
// Removal Handler
|
||||
document.getElementById('removeForm').addEventListener('submit', function (e) {
|
||||
// Grab all checked checkboxes from the Drive‑Health form
|
||||
// Grab all checked checkboxes from the Drive-Health form
|
||||
const checked = document.querySelectorAll('#driveHealthForm input[type="checkbox"][name="hosts[]"]:checked');
|
||||
const ids = Array.from(checked).map(cb => cb.value);
|
||||
|
||||
// Pass the comma‑separated list to the hidden input
|
||||
// Pass the comma-separated list to the hidden input
|
||||
document.getElementById('remove_hosts_input').value = ids.join(',');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,12 +6,25 @@
|
||||
--bg-body: #2c3e50; /* main page background */
|
||||
--bg-card: #34495e; /* card / panel background */
|
||||
--bg-sidebar: #3d566e; /* sidebar background (slightly lighter) */
|
||||
/* Accent / link colour */
|
||||
/* Accent / link color */
|
||||
--clr-accent: #3498db; /* blue accent for links */
|
||||
/* Text colour */
|
||||
/* Text color */
|
||||
--clr-text: #ecf0f1; /* light whiteish text */
|
||||
/* Borders / accents */
|
||||
--clr-border: #1f2b38; /* dark border colour */
|
||||
--clr-border: #1f2b38; /* dark border color */
|
||||
/* button colors*/
|
||||
--clr-btn-bg: var(--bg-card); /* default button background */
|
||||
--clr-btn-text: var(--clr-text); /* button text color */
|
||||
--clr-btn-border: var(--clr-border); /* border color */
|
||||
--clr-btn-accent: var(--clr-accent); /* hover/focus color */
|
||||
--clr-btn-shadow: rgba(0,0,0,0.3);
|
||||
/* select colors */
|
||||
--clr-select-bg: var(--bg-card); /* dropdown background */
|
||||
--clr-select-text: var(--clr-text); /* option text colour */
|
||||
--clr-select-border: var(--clr-border);
|
||||
--clr-select-accent: var(--clr-accent);/* focus / hover border */
|
||||
--clr-select-shadow: rgba(0,0,0,.3);
|
||||
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
@@ -84,7 +97,7 @@ table, th, td {
|
||||
}
|
||||
th, td { padding: 10px; }
|
||||
|
||||
/* Alternate row colour for metrics table */
|
||||
/* Alternate row color for metrics table */
|
||||
#host_metrics_table tbody tr td:nth-of-type(even) {
|
||||
background: #3e5c78; /* slight contrast */
|
||||
}
|
||||
@@ -154,7 +167,7 @@ li { margin-bottom: 10px; color: var(--clr-text); }
|
||||
margin-left: 6px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
background: #808080; /* default – unknown / no timestamp */
|
||||
background: #808080; /* default - unknown / no timestamp */
|
||||
transition: background-color 1s linear; /* smooth fade */
|
||||
}
|
||||
|
||||
@@ -258,3 +271,127 @@ li { margin-bottom: 10px; color: var(--clr-text); }
|
||||
|
||||
}
|
||||
|
||||
/* lets make buttons nice*/
|
||||
|
||||
/* 2. Base button style */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: .55rem 1.25rem;
|
||||
font-size: .9rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
color: var(--clr-btn-text);
|
||||
background: var(--clr-btn-bg);
|
||||
border: 1px solid var(--clr-btn-border);
|
||||
border-radius: .25rem;
|
||||
cursor: pointer;
|
||||
transition: background .15s ease, box-shadow .15s ease, transform .075s ease;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 3. Primary button - accent color */
|
||||
.btn-primary {
|
||||
background: var(--clr-btn-accent);
|
||||
border-color: var(--clr-btn-accent);
|
||||
color: #fff; /* brighter text for dark button */
|
||||
}
|
||||
|
||||
/* 4. Secondary button - subtle background */
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
border-color: var(--clr-btn-accent);
|
||||
color: var(--clr-btn-accent);
|
||||
}
|
||||
|
||||
/* 5. Hover & focus states */
|
||||
.btn:hover,
|
||||
.btn:focus-visible {
|
||||
background: #2b3f4c; /* a slightly darker shade of card bg */
|
||||
box-shadow: 0 3px 8px var(--clr-btn-shadow);
|
||||
transform: translateY(-1px);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* 6. Disabled state */
|
||||
.btn:disabled,
|
||||
.btn[aria-disabled="true"] {
|
||||
opacity: .55;
|
||||
cursor: not-allowed;
|
||||
background: var(--bg-card);
|
||||
border-color: var(--clr-border);
|
||||
}
|
||||
|
||||
/* 7. Extra - block‑level button */
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* let's also make the <select> element not suck*/
|
||||
|
||||
/* Base <select> style */
|
||||
.select-dark {
|
||||
appearance: none; /* hide native arrow in Chrome/Edge/FF */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
|
||||
display: inline-block;
|
||||
padding: .45rem 1.25rem .45rem .75rem;
|
||||
font-size: .9rem;
|
||||
line-height: 1.4;
|
||||
color: var(--clr-select-text);
|
||||
background: var(--clr-select-bg);
|
||||
border: 1px solid var(--clr-select-border);
|
||||
border-radius: .25rem;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
min-width: 150px; /* optional - forces a decent width */
|
||||
|
||||
/* Custom arrow - positioned after the element */
|
||||
background-image: linear-gradient(45deg, transparent 50%, var(--clr-select-accent) 50%),
|
||||
linear-gradient(135deg, var(--clr-select-accent) 50%, transparent 50%),
|
||||
linear-gradient(to right, var(--clr-select-border), var(--clr-select-border));
|
||||
background-position:
|
||||
calc(100% - 20px) calc(50% + 2px),
|
||||
calc(100% - 15px) calc(50% + 2px),
|
||||
calc(100% - 3px) 50%;
|
||||
background-size:
|
||||
5px 5px,
|
||||
5px 5px,
|
||||
1px 1.5rem;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* Hover & focus - subtle accent border */
|
||||
.select-dark:hover,
|
||||
.select-dark:focus-visible {
|
||||
border-color: var(--clr-select-accent);
|
||||
box-shadow: 0 0 3px var(--clr-select-shadow);
|
||||
outline: none; /* rely on the box-shadow for focus */
|
||||
}
|
||||
|
||||
/* Disabled state - greys out the control */
|
||||
.select-dark:disabled,
|
||||
.select-dark[aria-disabled="true"] {
|
||||
opacity: .55;
|
||||
cursor: not-allowed;
|
||||
background: var(--bg-card);
|
||||
border-color: var(--clr-border);
|
||||
}
|
||||
|
||||
/* <option> styling - works in most browsers */
|
||||
.select-dark option {
|
||||
background: var(--bg-card);
|
||||
color: var(--clr-select-text);
|
||||
}
|
||||
|
||||
/* Optional - make the arrow bigger on hover for visibility */
|
||||
.select-dark:hover {
|
||||
background-position:
|
||||
calc(100% - 18px) calc(50% + 2px),
|
||||
calc(100% - 13px) calc(50% + 2px),
|
||||
calc(100% - 3px) 50%;
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/* ---------- Global constants ---------- */
|
||||
define('API_KEY', 'deadbeef'); // API key used for the Remove button
|
||||
|
||||
/* ---------- Utility functions ---------- */
|
||||
date_default_timezone_set('America/Los_Angeles');
|
||||
/**
|
||||
* Escape HTML
|
||||
*/
|
||||
function h(string $s): string
|
||||
{
|
||||
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
/**
|
||||
* Load simple key/value pairs from a YAML file.
|
||||
* Lines starting with '#' are ignored.
|
||||
* The function returns an associative array.
|
||||
*/
|
||||
function loadYaml(string $path): array
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
return [];
|
||||
}
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$data = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
$pos = strpos($line, ':');
|
||||
if ($pos === false) {
|
||||
continue;
|
||||
}
|
||||
$key = trim(substr($line, 0, $pos));
|
||||
$value = trim(substr($line, $pos + 1));
|
||||
$value = trim($value, "\"'"); // remove surrounding quotes
|
||||
if ($value === '') {
|
||||
$value = null;
|
||||
}
|
||||
$data[$key] = $value;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
/* ---------- Load settings ---------- */
|
||||
$settingsPath = '/app/cosmostat_settings.yaml';
|
||||
$settings = loadYaml($settingsPath);
|
||||
|
||||
/* ---------- Page mode handling ---------- */
|
||||
$mode = $_GET['mode'] ?? 'cosmostat'; // default mode
|
||||
$validModes = ['cosmostat', 'drive_health']; // extend as needed
|
||||
if (!in_array($mode, $validModes, true)) {
|
||||
$mode = 'cosmostat';
|
||||
}
|
||||
|
||||
|
||||
/* ---------- Process a POST “remove” request ---------- */
|
||||
/* ---------- Helper: remove client details ---------- */
|
||||
function removeClient($clientList): array
|
||||
{
|
||||
$bindIp = "172.25.1.18";
|
||||
$port = "5001";
|
||||
$path = "/storage_client_delete";
|
||||
// Build the request payload.
|
||||
$payload = [
|
||||
'bind_ip' => $bindIp,
|
||||
'port' => $port,
|
||||
'path' => $path,
|
||||
'API_KEY' => API_KEY,
|
||||
'remove_hosts' => $clientList,
|
||||
];
|
||||
$jsonPayload = json_encode($payload);
|
||||
if ($jsonPayload === false) {
|
||||
// Encoding failed – return a clear error.
|
||||
return [
|
||||
'status' => 'error',
|
||||
'code' => 0,
|
||||
'message' => 'Failed to encode request payload: ' . json_last_error_msg(),
|
||||
];
|
||||
}
|
||||
|
||||
// Prepare the HTTP context for a POST request.
|
||||
$headers = [
|
||||
"Content-Type: application/json",
|
||||
"Content-Length: " . strlen($jsonPayload),
|
||||
"User-Agent: PHP/" . PHP_VERSION,
|
||||
];
|
||||
|
||||
$ctxOptions = [
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => implode("\r\n", $headers),
|
||||
'content' => $jsonPayload,
|
||||
'timeout' => 5,
|
||||
'ignore_errors' => true, // Still receive body for 4xx/5xx
|
||||
]
|
||||
];
|
||||
|
||||
$ctx = stream_context_create($ctxOptions);
|
||||
|
||||
|
||||
$url = "http://172.25.1.18:5001/storage_client_delete";
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 5,
|
||||
'header' => "User-Agent: PHP/" . PHP_VERSION . "\r\n"
|
||||
]
|
||||
]);
|
||||
$json = @file_get_contents($url, false, $ctx);
|
||||
if ($json === false) {
|
||||
return []; // caller will handle empty case
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
if (!is_array($data)) {
|
||||
return ['fail'];
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
$removedHostsMsg = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE
|
||||
&& isset($data['API_KEY'], $data['remove_hosts'])
|
||||
&& $data['API_KEY'] === API_KEY
|
||||
) {
|
||||
/* ---- For demo purposes we just echo a message. In a real
|
||||
system you would perform the removal logic here. ---- */
|
||||
$removedHostsMsg = '<div class="card"><p>Removed hosts: '
|
||||
. h(implode(', ', $data['remove_hosts']))
|
||||
. '</p></div>';
|
||||
echo "Time to remove ".$data['remove_hosts']."<br";
|
||||
// removeClient($clientList);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- API configuration per mode ---------- */
|
||||
$apiConfig = [
|
||||
'cosmostat' => ['bind' => '10.200.27.20', 'port' => '5000'],
|
||||
'drive_health' => ['bind' => '172.25.1.18', 'port' => '5001'], // new API
|
||||
];
|
||||
|
||||
/* ---------- Helper: fetch client details ---------- */
|
||||
function fetchClientDetails(string $bindIp, string $port, string $path = '/client_details'): array
|
||||
{
|
||||
$url = "http://{$bindIp}:{$port}{$path}";
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 5,
|
||||
'header' => "User-Agent: PHP/" . PHP_VERSION . "\r\n"
|
||||
]
|
||||
]);
|
||||
$json = @file_get_contents($url, false, $ctx);
|
||||
if ($json === false) {
|
||||
return []; // caller will handle empty case
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
if (!is_array($data)) {
|
||||
return ['fail'];
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
/* ---------- Fetch client details ---------- */
|
||||
$apiInfo = $apiConfig[$mode];
|
||||
$clients = fetchClientDetails($apiInfo['bind'], $apiInfo['port']);
|
||||
|
||||
/* ---------- Ensure each client has a short_id ---------- */
|
||||
foreach ($clients as &$client) {
|
||||
if (!isset($client['short_id'])) {
|
||||
$client['short_id'] = substr($client['uuid'] ?? '', 0, 8);
|
||||
}
|
||||
}
|
||||
unset($client);
|
||||
/* ---------- Determine selected hosts (Drive Health only) ---------- */
|
||||
$selectedHosts = $_GET['hosts'] ?? [];
|
||||
if ($mode === 'drive_health') {
|
||||
if (isset($_GET['action'])) {
|
||||
switch ($_GET['action']) {
|
||||
case 'all':
|
||||
$selectedHosts = array_column($clients, 'short_id');
|
||||
break;
|
||||
case 'none':
|
||||
$selectedHosts = [];
|
||||
break;
|
||||
// 'apply' – nothing to do; $selectedHosts already contains the posted hosts
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ---- Determine selected host ---- */
|
||||
$selectedId = $_GET['host'] ?? '';
|
||||
$selectedIdx = null;
|
||||
foreach ($clients as $idx => $client) {
|
||||
if (isset($client['short_id']) && $client['short_id'] === $selectedId) {
|
||||
$selectedIdx = $idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($selectedIdx === null) {
|
||||
// Default to the first client (if any)
|
||||
$selectedIdx = 0;
|
||||
$selectedId = $clients[$selectedIdx]['short_id'] ?? '';
|
||||
}
|
||||
$client = $clients[$selectedIdx] ?? null;
|
||||
$properties = $client['client_properties'][0] ?? [];
|
||||
$systemProperties = $properties['system_properties'] ?? [];
|
||||
$systemComponents = $properties['system_components'] ?? [];
|
||||
$selectedHost = $clients[$selectedIdx]['hostname'] ?? 'Unknown';
|
||||
|
||||
/* ---- ---- */
|
||||
/* ---- Sidebar Renderer ---- */
|
||||
function renderSidebar(string $mode){
|
||||
global $clients, $client, $properties, $systemProperties, $systemComponents, $selectedHost, $selectedHosts, $selectedId, $selectedIdx, $removedHostsMsg;
|
||||
/* Show a removal‑success message if we just processed a POST */
|
||||
if ($removedHostsMsg) {
|
||||
echo $removedHostsMsg;
|
||||
}
|
||||
$modes = [
|
||||
'cosmostat' => 'Cosmostat',
|
||||
'drive_health' => 'Drive Health',
|
||||
];
|
||||
?>
|
||||
<nav class="sidebar">
|
||||
<form method="get" id="modeForm">
|
||||
<label for="modeSelect">Mode:</label>
|
||||
<select name="mode" id="modeSelect" onchange="this.form.submit()">
|
||||
<?php foreach ($modes as $key => $label): ?>
|
||||
<option value="<?= h($key) ?>" <?= $mode === $key ? 'selected' : '' ?>>
|
||||
<?= h($label) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<?php if ($mode === 'drive_health'): ?>
|
||||
<?php
|
||||
if ( !is_array($clients) || empty($clients) ) {
|
||||
echo '<p style="margin:1rem 0; font-style:italic;">'
|
||||
. 'No hosts are available to manage at this time.'
|
||||
. '</p>';
|
||||
}
|
||||
?>
|
||||
<?php if (is_array($clients) && !empty($clients)): ?>
|
||||
<h3>Hosts</h3>
|
||||
<form method="get" id="driveHealthForm">
|
||||
<input type="hidden" name="mode" value="drive_health">
|
||||
<ul>
|
||||
<?php foreach ($clients as $c): ?>
|
||||
<?php $id = $c['short_id']; ?>
|
||||
<li>
|
||||
<label>
|
||||
<input type="checkbox" name="hosts[]" value="<?= h($id) ?>"
|
||||
<?= in_array($id, $selectedHosts, true) ? 'checked' : '' ?>>
|
||||
<?= h($c['name']) ?>
|
||||
</label>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul><p>
|
||||
<button type="submit" name="action" value="apply">Apply</button><p>
|
||||
<button type="submit" name="action" value="all">Select All</button><br>
|
||||
<button type="submit" name="action" value="none">Select None</button><p>
|
||||
</form>
|
||||
|
||||
<!-- Remove button (POST → same page) -->
|
||||
<form id="removeHostsForm" method="post" action="">
|
||||
<!-- API key is added by JS; we keep this form so the button can be inside it -->
|
||||
<button type="button" id="removeButton">Remove Selected Hosts</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($mode == 'cosmostat'): ?>
|
||||
<input type="hidden" name="host" value="<?= h($selectedId) ?>">
|
||||
<h3>Endpoints</h3>
|
||||
<ol id="endpointList"></ol>
|
||||
<?php endif; ?>
|
||||
<?php if ($mode == 'gali'): ?>
|
||||
<h3>Shuttle Gali</h3>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php
|
||||
}
|
||||
|
||||
/* ---- Main Content Renderer ---- */
|
||||
function renderMainContent(string $mode){
|
||||
global $clients, $client, $properties, $systemProperties, $systemComponents, $selectedHost, $selectedHosts, $selectedId, $selectedIdx;
|
||||
/* ---------- Handle empty / error ---------- */
|
||||
if ($clients === ['fail']) {
|
||||
die('<div class="card"><p style="color:red;">Could not retrieve data from the API for mode "' . h($mode) . '".</p></div>');
|
||||
}
|
||||
if ($mode === 'drive_health') {
|
||||
// If nothing is selected, show a friendly message
|
||||
if (empty($selectedHosts)) {
|
||||
echo '<div class="card"><p>No hosts selected.</p></div>';
|
||||
return;
|
||||
}
|
||||
echo '<div class="storage_client">';
|
||||
foreach ($selectedHosts as $sid) {
|
||||
// Find the client that matches this short_id
|
||||
$c = null;
|
||||
foreach ($clients as $cl) {
|
||||
if ($cl['short_id'] === $sid) {
|
||||
$c = $cl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($c === null) continue; // safety
|
||||
$hostname = $c['name'] ?? 'Unknown';
|
||||
echo '<div class="card">';
|
||||
echo '<h2>Drive Health - ' . h($hostname) . '</h2>';
|
||||
echo '<h3>IP: ' . h($c['ip']) . '<br>';
|
||||
echo 'Timestamp: ' . date('F j, Y g:i a', (int) $c['timestamp']) . '</h3>';
|
||||
if (isset($c['drives']) && is_array($c['drives']) && count($c['drives']) > 0) {
|
||||
echo '<table id="host_metrics_table">';
|
||||
echo '<thead><tr><th>Drive Letter</th><th>Model</th><th>Capacity</th><th>Power On Hours</th><th>Host Writes</th><th>Wear Level</th></tr></thead>';
|
||||
echo '<tbody>';
|
||||
foreach ($c['drives'] as $drive) {
|
||||
echo '<tr>';
|
||||
echo '<td>' . h($drive['drive_letter'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['model'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['capacity'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['power_on_hours'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['host_writes'] ?? '') . '</td>';
|
||||
echo '<td>' . h($drive['wear_level'] ?? '') . '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
} else {
|
||||
echo '<p>No drive data available for this host.</p>';
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="main">
|
||||
<?php if ($mode == 'cosmostat'): ?><!-- Header Card -->
|
||||
<div class="card">
|
||||
<h2>Matt-Cloud Cosmostat Dashboard</h2>
|
||||
<p>This dashboard shows the local Matt-Cloud system stats.</p>
|
||||
<div class="help-link" id="helpToggle">API</div>
|
||||
</div> <!-- / Header Card -->
|
||||
<!-- Hidden API Card -->
|
||||
<div id="helpText" class="card">
|
||||
<strong>Component Desriptor</strong>
|
||||
<p>To view the component descriptor, you may <br>
|
||||
<code>curl -s https://<?= h($_SERVER['SERVER_NAME']) ?>/descriptor</code></p>
|
||||
This will return the entire JSON descriptor variable.<br>
|
||||
The agent then reports back to the Cosmostat Server with all the data found in the descriptor.<br>
|
||||
Full Source Code can be found at its <a target="_blank" rel="noopener noreferrer" href="https://gitea.matt-cloud.com/matt/cosmoserver">Gitea</a> page.
|
||||
</div> <!-- / Header Card -->
|
||||
<!-- summary card -->
|
||||
<div class="card">
|
||||
<?php if (!empty($systemProperties)): ?>
|
||||
<h2>System Properties</h2>
|
||||
<table><tr>
|
||||
<td>
|
||||
<ul class="system-list">
|
||||
<?php foreach ($systemProperties as $prop): ?>
|
||||
<li><?= h($prop['Property']) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<h2>Live System Metrics</h2>
|
||||
<div id="host_metrics" class="column">Connecting...</div>
|
||||
</td>
|
||||
</tr></table>
|
||||
<?php endif; ?><br>
|
||||
<div class="componentDetail-link" id="componentDetailToggle">Toggle Component Details</div>
|
||||
</div> <!--/summary card -->
|
||||
|
||||
<!-- hidden detail card -->
|
||||
<div id="componentDetailText" class="card">
|
||||
<?php if (!empty($systemComponents)): ?>
|
||||
<h2>Components</h2>
|
||||
<div class="components">
|
||||
<?php foreach ($systemComponents as $comp): ?>
|
||||
<div class="component">
|
||||
<h3><?= h($comp['component_name']) ?></h3>
|
||||
<ul class="info-list">
|
||||
<?php foreach ($comp['info_strings'] as $info): ?>
|
||||
<li><?= h($info) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div> <!--/hidden detail card -->
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($mode == 'gali'): ?>
|
||||
<h3>Shuttle Gali</h3>
|
||||
<?php endif; ?>
|
||||
|
||||
</div> <!-- /main -->
|
||||
<?php
|
||||
}
|
||||
|
||||
/* ---- Render server dashboard ---- */
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Cosmostat - <?= h($selectedHost) ?></title>
|
||||
<link rel="stylesheet" href="src/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<?php renderSidebar($mode); ?>
|
||||
<!-- Main content -->
|
||||
<?php renderMainContent($mode); ?>
|
||||
|
||||
</div> <!-- /wrapper -->
|
||||
<script src="socket.io/socket.io.js"></script>
|
||||
<script src="src/system_metrics.js"></script>
|
||||
<script>
|
||||
/* Help / detail toggles */
|
||||
document.getElementById('helpToggle')?.addEventListener('click', function () {
|
||||
const help = document.getElementById('helpText');
|
||||
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
|
||||
});
|
||||
document.getElementById('componentDetailToggle')?.addEventListener('click', function () {
|
||||
const help = document.getElementById('componentDetailText');
|
||||
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
/* --- Remove button logic --- */
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const removeBtn = document.getElementById('removeButton');
|
||||
if (!removeBtn) return; // not in drive_health mode
|
||||
|
||||
removeBtn.addEventListener('click', function () {
|
||||
// Grab all checked hosts from the drive‑health form
|
||||
const checked = document.querySelectorAll('input[name="hosts[]"]:checked');
|
||||
const hosts = Array.from(checked).map(cb => cb.value);
|
||||
|
||||
if (hosts.length === 0) {
|
||||
alert('No hosts selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
API_KEY: '<?= API_KEY ?>',
|
||||
remove_hosts: hosts
|
||||
};
|
||||
|
||||
fetch('', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(resp => {
|
||||
if (!resp.ok) throw new Error('Network error');
|
||||
return resp.text(); // we just reload on any response
|
||||
})
|
||||
.then(() => {
|
||||
// reload the same index page
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert('Error removing hosts.');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# this was some weird testing stuff
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Grab the list of IPs
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user