2026-01-12 14:05:06 -03:00
|
|
|
<link rel="stylesheet" href="monitoring/services.css" />
|
|
|
|
|
|
2025-12-06 11:09:12 -03:00
|
|
|
<div class="services-container">
|
|
|
|
|
<!-- Services Header -->
|
|
|
|
|
<div class="services-header">
|
2026-01-12 14:05:06 -03:00
|
|
|
<div
|
|
|
|
|
class="header-stats"
|
|
|
|
|
hx-get="/api/services/summary"
|
|
|
|
|
hx-trigger="load, every 10s"
|
|
|
|
|
hx-swap="innerHTML"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<div class="stat-item running">
|
|
|
|
|
<span class="stat-number">--</span>
|
|
|
|
|
<span class="stat-label">Running</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item warning">
|
|
|
|
|
<span class="stat-number">--</span>
|
|
|
|
|
<span class="stat-label">Warning</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item stopped">
|
|
|
|
|
<span class="stat-number">--</span>
|
|
|
|
|
<span class="stat-label">Stopped</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item total">
|
|
|
|
|
<span class="stat-number">--</span>
|
|
|
|
|
<span class="stat-label">Total</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="header-actions">
|
|
|
|
|
<div class="search-box">
|
2026-01-12 14:05:06 -03:00
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
|
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
|
|
|
</svg>
|
2026-01-12 14:05:06 -03:00
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
id="service-search"
|
|
|
|
|
placeholder="Search services..."
|
|
|
|
|
onkeyup="filterServices(this.value)"
|
|
|
|
|
/>
|
2025-12-06 11:09:12 -03:00
|
|
|
</div>
|
|
|
|
|
<select id="status-filter" onchange="filterByStatus(this.value)">
|
|
|
|
|
<option value="all">All Status</option>
|
|
|
|
|
<option value="running">Running</option>
|
|
|
|
|
<option value="warning">Warning</option>
|
|
|
|
|
<option value="stopped">Stopped</option>
|
|
|
|
|
</select>
|
2026-01-12 14:05:06 -03:00
|
|
|
<button
|
|
|
|
|
class="action-btn"
|
|
|
|
|
onclick="restartAllServices()"
|
|
|
|
|
title="Restart All"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<polyline points="23 4 23 10 17 10"></polyline>
|
|
|
|
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
Restart All
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Services Grid -->
|
2026-01-12 14:05:06 -03:00
|
|
|
<div
|
|
|
|
|
class="services-grid"
|
|
|
|
|
id="services-grid"
|
|
|
|
|
hx-get="/api/services/status"
|
|
|
|
|
hx-trigger="load, every 10s"
|
|
|
|
|
hx-swap="innerHTML"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<!-- Loading placeholder -->
|
|
|
|
|
<div class="service-card skeleton">
|
|
|
|
|
<div class="skeleton-line"></div>
|
|
|
|
|
<div class="skeleton-line short"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="service-card skeleton">
|
|
|
|
|
<div class="skeleton-line"></div>
|
|
|
|
|
<div class="skeleton-line short"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="service-card skeleton">
|
|
|
|
|
<div class="skeleton-line"></div>
|
|
|
|
|
<div class="skeleton-line short"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="service-card skeleton">
|
|
|
|
|
<div class="skeleton-line"></div>
|
|
|
|
|
<div class="skeleton-line short"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Service Detail Panel (slides in from right) -->
|
|
|
|
|
<div class="service-detail-panel" id="service-detail-panel">
|
|
|
|
|
<div class="panel-header">
|
|
|
|
|
<h3 id="detail-service-name">Service Details</h3>
|
|
|
|
|
<button class="close-btn" onclick="closeServiceDetail()">
|
2026-01-12 14:05:06 -03:00
|
|
|
<svg
|
|
|
|
|
width="20"
|
|
|
|
|
height="20"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
|
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="panel-content" id="service-detail-content">
|
|
|
|
|
<!-- Loaded via HTMX -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Service Card Template (for reference - rendered by server) -->
|
|
|
|
|
<template id="service-card-template">
|
|
|
|
|
<div class="service-card" data-status="running" data-service="service-name">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<div class="service-icon">
|
2026-01-12 14:05:06 -03:00
|
|
|
<svg
|
|
|
|
|
width="24"
|
|
|
|
|
height="24"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<rect
|
|
|
|
|
x="2"
|
|
|
|
|
y="2"
|
|
|
|
|
width="20"
|
|
|
|
|
height="8"
|
|
|
|
|
rx="2"
|
|
|
|
|
ry="2"
|
|
|
|
|
></rect>
|
|
|
|
|
<rect
|
|
|
|
|
x="2"
|
|
|
|
|
y="14"
|
|
|
|
|
width="20"
|
|
|
|
|
height="8"
|
|
|
|
|
rx="2"
|
|
|
|
|
ry="2"
|
|
|
|
|
></rect>
|
2025-12-06 11:09:12 -03:00
|
|
|
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
|
|
|
|
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="status-badge running">
|
|
|
|
|
<span class="status-dot"></span>
|
|
|
|
|
<span class="status-text">Running</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<h4 class="service-name">Service Name</h4>
|
|
|
|
|
<p class="service-description">Service description goes here</p>
|
|
|
|
|
<div class="service-meta">
|
|
|
|
|
<span class="meta-item">
|
2026-01-12 14:05:06 -03:00
|
|
|
<svg
|
|
|
|
|
width="14"
|
|
|
|
|
height="14"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
|
|
|
<polyline points="12 6 12 12 16 14"></polyline>
|
|
|
|
|
</svg>
|
|
|
|
|
Uptime: 24d 5h
|
|
|
|
|
</span>
|
|
|
|
|
<span class="meta-item">
|
2026-01-12 14:05:06 -03:00
|
|
|
<svg
|
|
|
|
|
width="14"
|
|
|
|
|
height="14"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
|
|
|
|
<rect x="9" y="9" width="6" height="6"></rect>
|
|
|
|
|
</svg>
|
|
|
|
|
CPU: 12%
|
|
|
|
|
</span>
|
|
|
|
|
<span class="meta-item">
|
2026-01-12 14:05:06 -03:00
|
|
|
<svg
|
|
|
|
|
width="14"
|
|
|
|
|
height="14"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<rect x="2" y="6" width="20" height="12" rx="2"></rect>
|
|
|
|
|
</svg>
|
|
|
|
|
Mem: 256MB
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-actions">
|
2026-01-12 14:05:06 -03:00
|
|
|
<button
|
|
|
|
|
class="card-btn"
|
|
|
|
|
onclick="viewServiceDetails('service-id')"
|
|
|
|
|
title="Details"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
|
|
|
<line x1="12" y1="16" x2="12" y2="12"></line>
|
|
|
|
|
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-01-12 14:05:06 -03:00
|
|
|
<button
|
|
|
|
|
class="card-btn"
|
|
|
|
|
onclick="restartService('service-id')"
|
|
|
|
|
title="Restart"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<polyline points="23 4 23 10 17 10"></polyline>
|
|
|
|
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-01-12 14:05:06 -03:00
|
|
|
<button
|
|
|
|
|
class="card-btn"
|
|
|
|
|
onclick="stopService('service-id')"
|
|
|
|
|
title="Stop"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
2025-12-06 11:09:12 -03:00
|
|
|
<rect x="6" y="4" width="4" height="16"></rect>
|
|
|
|
|
<rect x="14" y="4" width="4" height="16"></rect>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-01-12 14:05:06 -03:00
|
|
|
<button
|
|
|
|
|
class="card-btn"
|
|
|
|
|
onclick="viewServiceLogs('service-id')"
|
|
|
|
|
title="Logs"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
|
|
|
|
></path>
|
2025-12-06 11:09:12 -03:00
|
|
|
<polyline points="14 2 14 8 20 8"></polyline>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
refactor: Extract inline CSS/JS to separate files for monitoring module
- Create individual CSS files: monitoring.css, alerts.css, health.css, logs.css, metrics.css, resources.css
- Create individual JS files: monitoring.js, alerts.js, health.js, logs.js, metrics.js, resources.js
- Update HTML files to reference external CSS/JS files
- Add CSS/JS files for other modules (analytics, chat, mail, meet, tasks, etc.)
- Remove obsolete implementation plan files
2025-12-07 09:56:27 -03:00
|
|
|
<!-- CSS moved to services.css -->
|
2025-12-06 11:09:12 -03:00
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
function filterServices(query) {
|
2026-01-12 14:05:06 -03:00
|
|
|
const cards = document.querySelectorAll(".service-card:not(.skeleton)");
|
2025-12-06 11:09:12 -03:00
|
|
|
const lowerQuery = query.toLowerCase();
|
|
|
|
|
|
2026-01-12 14:05:06 -03:00
|
|
|
cards.forEach((card) => {
|
|
|
|
|
const name = card.dataset.service?.toLowerCase() || "";
|
2025-12-06 11:09:12 -03:00
|
|
|
const text = card.textContent.toLowerCase();
|
2026-01-12 14:05:06 -03:00
|
|
|
const matches =
|
|
|
|
|
name.includes(lowerQuery) || text.includes(lowerQuery);
|
|
|
|
|
card.classList.toggle("hidden", !matches);
|
2025-12-06 11:09:12 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterByStatus(status) {
|
2026-01-12 14:05:06 -03:00
|
|
|
const cards = document.querySelectorAll(".service-card:not(.skeleton)");
|
2025-12-06 11:09:12 -03:00
|
|
|
|
2026-01-12 14:05:06 -03:00
|
|
|
cards.forEach((card) => {
|
|
|
|
|
if (status === "all") {
|
|
|
|
|
card.classList.remove("hidden");
|
2025-12-06 11:09:12 -03:00
|
|
|
} else {
|
|
|
|
|
const cardStatus = card.dataset.status;
|
2026-01-12 14:05:06 -03:00
|
|
|
card.classList.toggle("hidden", cardStatus !== status);
|
2025-12-06 11:09:12 -03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function viewServiceDetails(serviceId) {
|
2026-01-12 14:05:06 -03:00
|
|
|
const panel = document.getElementById("service-detail-panel");
|
|
|
|
|
const content = document.getElementById("service-detail-content");
|
2025-12-06 11:09:12 -03:00
|
|
|
|
|
|
|
|
// Load service details via HTMX
|
2026-01-12 14:05:06 -03:00
|
|
|
htmx.ajax("GET", `/api/services/${serviceId}/details`, {
|
2025-12-06 11:09:12 -03:00
|
|
|
target: content,
|
2026-01-12 14:05:06 -03:00
|
|
|
swap: "innerHTML",
|
2025-12-06 11:09:12 -03:00
|
|
|
});
|
|
|
|
|
|
2026-01-12 14:05:06 -03:00
|
|
|
document.getElementById("detail-service-name").textContent = serviceId;
|
|
|
|
|
panel.classList.add("open");
|
2025-12-06 11:09:12 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeServiceDetail() {
|
2026-01-12 14:05:06 -03:00
|
|
|
document
|
|
|
|
|
.getElementById("service-detail-panel")
|
|
|
|
|
.classList.remove("open");
|
2025-12-06 11:09:12 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function restartService(serviceId) {
|
|
|
|
|
if (confirm(`Are you sure you want to restart ${serviceId}?`)) {
|
2026-01-12 14:05:06 -03:00
|
|
|
htmx.ajax("POST", `/api/services/${serviceId}/restart`, {
|
|
|
|
|
swap: "none",
|
2025-12-06 11:09:12 -03:00
|
|
|
}).then(() => {
|
2026-01-12 14:05:06 -03:00
|
|
|
htmx.trigger("#services-grid", "refresh");
|
2025-12-06 11:09:12 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopService(serviceId) {
|
|
|
|
|
if (confirm(`Are you sure you want to stop ${serviceId}?`)) {
|
2026-01-12 14:05:06 -03:00
|
|
|
htmx.ajax("POST", `/api/services/${serviceId}/stop`, {
|
|
|
|
|
swap: "none",
|
2025-12-06 11:09:12 -03:00
|
|
|
}).then(() => {
|
2026-01-12 14:05:06 -03:00
|
|
|
htmx.trigger("#services-grid", "refresh");
|
2025-12-06 11:09:12 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startService(serviceId) {
|
2026-01-12 14:05:06 -03:00
|
|
|
htmx.ajax("POST", `/api/services/${serviceId}/start`, {
|
|
|
|
|
swap: "none",
|
2025-12-06 11:09:12 -03:00
|
|
|
}).then(() => {
|
2026-01-12 14:05:06 -03:00
|
|
|
htmx.trigger("#services-grid", "refresh");
|
2025-12-06 11:09:12 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function viewServiceLogs(serviceId) {
|
|
|
|
|
// Navigate to logs with service filter
|
|
|
|
|
const logsLink = document.querySelector('.nav-item[href="#logs"]');
|
|
|
|
|
if (logsLink) {
|
|
|
|
|
logsLink.click();
|
|
|
|
|
setTimeout(() => {
|
2026-01-12 14:05:06 -03:00
|
|
|
const serviceFilter = document.getElementById("service-filter");
|
2025-12-06 11:09:12 -03:00
|
|
|
if (serviceFilter) {
|
|
|
|
|
serviceFilter.value = serviceId;
|
2026-01-12 14:05:06 -03:00
|
|
|
serviceFilter.dispatchEvent(new Event("change"));
|
2025-12-06 11:09:12 -03:00
|
|
|
}
|
|
|
|
|
}, 300);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function restartAllServices() {
|
2026-01-12 14:05:06 -03:00
|
|
|
if (
|
|
|
|
|
confirm(
|
|
|
|
|
"Are you sure you want to restart all services? This may cause temporary downtime.",
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
htmx.ajax("POST", "/api/services/restart-all", {
|
|
|
|
|
swap: "none",
|
2025-12-06 11:09:12 -03:00
|
|
|
}).then(() => {
|
2026-01-12 14:05:06 -03:00
|
|
|
htmx.trigger("#services-grid", "refresh");
|
2025-12-06 11:09:12 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close panel on escape key
|
2026-01-12 14:05:06 -03:00
|
|
|
document.addEventListener("keydown", function (e) {
|
|
|
|
|
if (e.key === "Escape") {
|
2025-12-06 11:09:12 -03:00
|
|
|
closeServiceDetail();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close panel when clicking outside
|
2026-01-12 14:05:06 -03:00
|
|
|
document.addEventListener("click", function (e) {
|
|
|
|
|
const panel = document.getElementById("service-detail-panel");
|
|
|
|
|
if (
|
|
|
|
|
panel.classList.contains("open") &&
|
2025-12-06 11:09:12 -03:00
|
|
|
!panel.contains(e.target) &&
|
2026-01-12 14:05:06 -03:00
|
|
|
!e.target.closest(".card-btn")
|
|
|
|
|
) {
|
2025-12-06 11:09:12 -03:00
|
|
|
closeServiceDetail();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
</script>
|