Add per-server notes field and show hardware info on dashboard
This commit is contained in:
12
app/app.py
12
app/app.py
@@ -39,6 +39,7 @@ class Server(db.Model):
|
|||||||
is_online = db.Column(db.Boolean, default=False)
|
is_online = db.Column(db.Boolean, default=False)
|
||||||
last_collected = db.Column(db.DateTime, nullable=True)
|
last_collected = db.Column(db.DateTime, nullable=True)
|
||||||
details = db.Column(db.JSON, nullable=True)
|
details = db.Column(db.JSON, nullable=True)
|
||||||
|
notes = db.Column(db.Text, default='')
|
||||||
__table_args__ = (db.UniqueConstraint('username', 'hostname', name='uq_user_host'),)
|
__table_args__ = (db.UniqueConstraint('username', 'hostname', name='uq_user_host'),)
|
||||||
|
|
||||||
|
|
||||||
@@ -282,11 +283,22 @@ def api_servers():
|
|||||||
'url': s.url,
|
'url': s.url,
|
||||||
'is_online': s.is_online,
|
'is_online': s.is_online,
|
||||||
'last_collected': s.last_collected.isoformat() if s.last_collected else None,
|
'last_collected': s.last_collected.isoformat() if s.last_collected else None,
|
||||||
|
'notes': s.notes,
|
||||||
'details': s.details,
|
'details': s.details,
|
||||||
})
|
})
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/servers/<int:server_id>/notes', methods=['PUT'])
|
||||||
|
def api_update_notes(server_id):
|
||||||
|
from flask import request
|
||||||
|
server = Server.query.get_or_404(server_id)
|
||||||
|
data = request.get_json()
|
||||||
|
server.notes = data.get('notes', '')
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
def _ip_sort_key(ip_str):
|
def _ip_sort_key(ip_str):
|
||||||
if not ip_str:
|
if not ip_str:
|
||||||
return [999, 999, 999, 999]
|
return [999, 999, 999, 999]
|
||||||
|
|||||||
@@ -288,6 +288,36 @@ main {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Notes --- */
|
||||||
|
|
||||||
|
.notes-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 32px;
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #1e3a5f;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 8px 10px;
|
||||||
|
resize: none;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-input::placeholder {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Container / VM Sub-cards --- */
|
/* --- Container / VM Sub-cards --- */
|
||||||
|
|
||||||
.container-grid {
|
.container-grid {
|
||||||
|
|||||||
@@ -55,6 +55,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="server-ip">{{ server.primary_ip or 'No IP' }}</div>
|
<div class="server-ip">{{ server.primary_ip or 'No IP' }}</div>
|
||||||
<div class="server-os">{{ sys.get('os_pretty', '') }}</div>
|
<div class="server-os">{{ sys.get('os_pretty', '') }}</div>
|
||||||
|
{% if server.is_online and (cpu.get('model') or mem.get('total_mb')) %}
|
||||||
|
<div class="server-hw">
|
||||||
|
{%- if cpu.get('model') %}{{ cpu.get('model') }}{% endif %}
|
||||||
|
{%- if cpu.get('cores') %} ({{ cpu.get('cores') }}c){% endif %}
|
||||||
|
{%- if mem.get('total_mb') %} / {{ mem.get('total_mb', '')|format_mb }}{% endif -%}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if server.is_online %}
|
{% if server.is_online %}
|
||||||
<div class="usage-bars">
|
<div class="usage-bars">
|
||||||
@@ -96,6 +103,13 @@
|
|||||||
|
|
||||||
<!-- Expanded Details -->
|
<!-- Expanded Details -->
|
||||||
<div class="card-details" style="display: none;">
|
<div class="card-details" style="display: none;">
|
||||||
|
<div class="notes-section">
|
||||||
|
<textarea class="notes-input"
|
||||||
|
data-server-id="{{ server.id }}"
|
||||||
|
placeholder="Add notes..."
|
||||||
|
onclick="event.stopPropagation();"
|
||||||
|
oninput="autoResizeNotes(this); debounceSaveNotes(this);">{{ server.notes or '' }}</textarea>
|
||||||
|
</div>
|
||||||
<div class="details-grid">
|
<div class="details-grid">
|
||||||
<!-- System Info -->
|
<!-- System Info -->
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
@@ -170,6 +184,7 @@
|
|||||||
<tr class="table-header"><td>Interface</td><td>IPv4</td><td>IPv6</td><td>MAC</td><td>State</td><td>Speed</td><td>Driver</td></tr>
|
<tr class="table-header"><td>Interface</td><td>IPv4</td><td>IPv6</td><td>MAC</td><td>State</td><td>Speed</td><td>Driver</td></tr>
|
||||||
{% set nets = d.get('net', []) if d.get('net') else [] %}
|
{% set nets = d.get('net', []) if d.get('net') else [] %}
|
||||||
{% for iface in nets %}
|
{% for iface in nets %}
|
||||||
|
{% if iface.get('ipv4') or iface.get('ipv6') %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ iface.get('name', iface.get('_name', '-')) }}</td>
|
<td>{{ iface.get('name', iface.get('_name', '-')) }}</td>
|
||||||
<td>{{ iface.get('ipv4', '-') or '-' }}</td>
|
<td>{{ iface.get('ipv4', '-') or '-' }}</td>
|
||||||
@@ -179,6 +194,7 @@
|
|||||||
<td>{% if iface.get('speed_mbps') %}{{ iface.get('speed_mbps') }} Mbps{% else %}-{% endif %}</td>
|
<td>{% if iface.get('speed_mbps') %}{{ iface.get('speed_mbps') }} Mbps{% else %}-{% endif %}</td>
|
||||||
<td>{{ iface.get('driver', '-') or '-' }}</td>
|
<td>{{ iface.get('driver', '-') or '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -322,8 +338,32 @@
|
|||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
details.style.display = 'block';
|
details.style.display = 'block';
|
||||||
card.classList.add('expanded');
|
card.classList.add('expanded');
|
||||||
|
// Auto-resize notes textarea
|
||||||
|
const ta = details.querySelector('.notes-input');
|
||||||
|
if (ta) autoResizeNotes(ta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function autoResizeNotes(ta) {
|
||||||
|
ta.style.height = 'auto';
|
||||||
|
ta.style.height = ta.scrollHeight + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
let _notesTimers = {};
|
||||||
|
function debounceSaveNotes(ta) {
|
||||||
|
const id = ta.dataset.serverId;
|
||||||
|
clearTimeout(_notesTimers[id]);
|
||||||
|
_notesTimers[id] = setTimeout(() => saveNotes(ta), 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveNotes(ta) {
|
||||||
|
const id = ta.dataset.serverId;
|
||||||
|
fetch('/api/servers/' + id + '/notes', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({notes: ta.value})
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user