Add dashboard drag-to-reorder and Discord service grouping
This commit is contained in:
parent
e6fbd670ac
commit
65842e6d33
4 changed files with 127 additions and 22 deletions
|
|
@ -198,11 +198,32 @@
|
||||||
|
|
||||||
.service-row {
|
.service-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(130px, 0.8fr) minmax(220px, 1.5fr) minmax(180px, 1.2fr) 116px 88px 120px 40px;
|
grid-template-columns: 24px minmax(100px, 0.6fr) minmax(120px, 0.8fr) minmax(200px, 1.5fr) minmax(160px, 1.2fr) 100px 80px 110px 65px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
|
border-top: 2px solid transparent;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-row.drag-over {
|
||||||
|
border-top-color: var(--ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
color: var(--faint);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: center;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field label {
|
.field label {
|
||||||
|
|
@ -426,6 +447,7 @@
|
||||||
function normalizeService(service = {}) {
|
function normalizeService(service = {}) {
|
||||||
return {
|
return {
|
||||||
name: service.name || "",
|
name: service.name || "",
|
||||||
|
group: service.group || "Main Services",
|
||||||
url: service.url || "",
|
url: service.url || "",
|
||||||
displayUrl: service.displayUrl || service.url || "",
|
displayUrl: service.displayUrl || service.url || "",
|
||||||
method: service.method || "GET",
|
method: service.method || "GET",
|
||||||
|
|
@ -454,8 +476,14 @@
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "service-row";
|
row.className = "service-row";
|
||||||
row.dataset.index = String(index);
|
row.dataset.index = String(index);
|
||||||
|
row.draggable = true;
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
|
<div class="drag-handle" title="Drag to reorder">≡</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Group</label>
|
||||||
|
<input data-key="group" value="${escapeAttr(service.group)}">
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
<input data-key="name" value="${escapeAttr(service.name)}">
|
<input data-key="name" value="${escapeAttr(service.name)}">
|
||||||
|
|
@ -479,7 +507,7 @@
|
||||||
<div class="row-status">
|
<div class="row-status">
|
||||||
${renderResult(result)}
|
${renderResult(result)}
|
||||||
</div>
|
</div>
|
||||||
<button class="danger" type="button" data-remove="${index}" title="Remove service">Remove</button>
|
<button class="danger" type="button" data-remove="${index}" title="Remove service">Del</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
|
|
@ -636,6 +664,44 @@
|
||||||
renderServices();
|
renderServices();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let draggedRow = null;
|
||||||
|
|
||||||
|
servicesEl.addEventListener("dragstart", (e) => {
|
||||||
|
const row = e.target.closest(".service-row");
|
||||||
|
if (!row) return;
|
||||||
|
draggedRow = row;
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
|
// Save current input values back to the services array before moving
|
||||||
|
services = collectServices();
|
||||||
|
});
|
||||||
|
|
||||||
|
servicesEl.addEventListener("dragover", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const row = e.target.closest(".service-row");
|
||||||
|
if (!row || row === draggedRow) return;
|
||||||
|
e.dataTransfer.dropEffect = "move";
|
||||||
|
row.classList.add("drag-over");
|
||||||
|
});
|
||||||
|
|
||||||
|
servicesEl.addEventListener("dragleave", (e) => {
|
||||||
|
const row = e.target.closest(".service-row");
|
||||||
|
if (row) row.classList.remove("drag-over");
|
||||||
|
});
|
||||||
|
|
||||||
|
servicesEl.addEventListener("drop", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetRow = e.target.closest(".service-row");
|
||||||
|
if (targetRow) targetRow.classList.remove("drag-over");
|
||||||
|
if (!draggedRow || !targetRow || draggedRow === targetRow) return;
|
||||||
|
|
||||||
|
const fromIndex = parseInt(draggedRow.dataset.index, 10);
|
||||||
|
const toIndex = parseInt(targetRow.dataset.index, 10);
|
||||||
|
|
||||||
|
const item = services.splice(fromIndex, 1)[0];
|
||||||
|
services.splice(toIndex, 0, item);
|
||||||
|
renderServices();
|
||||||
|
});
|
||||||
|
|
||||||
loadSession().then(loadStatus).catch((error) => {
|
loadSession().then(loadStatus).catch((error) => {
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
showLogin();
|
showLogin();
|
||||||
|
|
|
||||||
15
preview.html
15
preview.html
|
|
@ -165,6 +165,14 @@
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.embed-field {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-field.inline {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
.embed-field-name {
|
.embed-field-name {
|
||||||
color: #f2f3f5;
|
color: #f2f3f5;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
|
|
@ -320,7 +328,9 @@
|
||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, ">");
|
.replace(/>/g, ">");
|
||||||
|
|
||||||
return escaped.replace(/\[([^\]]+)]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2">$1</a>');
|
return escaped
|
||||||
|
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/\[([^\]]+)]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2">$1</a>');
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(payload) {
|
function render(payload) {
|
||||||
|
|
@ -350,6 +360,9 @@
|
||||||
embed.fields.forEach((field) => {
|
embed.fields.forEach((field) => {
|
||||||
const item = document.createElement("div");
|
const item = document.createElement("div");
|
||||||
item.className = "embed-field";
|
item.className = "embed-field";
|
||||||
|
if (field.inline) {
|
||||||
|
item.classList.add("inline");
|
||||||
|
}
|
||||||
const name = document.createElement("div");
|
const name = document.createElement("div");
|
||||||
name.className = "embed-field-name";
|
name.className = "embed-field-name";
|
||||||
name.textContent = field.name || "\u200b";
|
name.textContent = field.name || "\u200b";
|
||||||
|
|
|
||||||
|
|
@ -2,36 +2,42 @@
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "Portal",
|
"name": "Portal",
|
||||||
|
"group": "Main Services",
|
||||||
"url": "https://mithraic.cloud",
|
"url": "https://mithraic.cloud",
|
||||||
"displayUrl": "https://mithraic.cloud",
|
"displayUrl": "https://mithraic.cloud",
|
||||||
"expectedStatuses": ["200-399"]
|
"expectedStatuses": ["200-399"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Jellyfin",
|
"name": "Jellyfin",
|
||||||
|
"group": "Main Services",
|
||||||
"url": "https://jellyfin.mithraic.cloud/health",
|
"url": "https://jellyfin.mithraic.cloud/health",
|
||||||
"displayUrl": "https://jellyfin.mithraic.cloud",
|
"displayUrl": "https://jellyfin.mithraic.cloud",
|
||||||
"expectedStatuses": ["200-399"]
|
"expectedStatuses": ["200-399"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Navidrome",
|
"name": "Navidrome",
|
||||||
|
"group": "Main Services",
|
||||||
"url": "https://listen.mithraic.cloud",
|
"url": "https://listen.mithraic.cloud",
|
||||||
"displayUrl": "https://listen.mithraic.cloud",
|
"displayUrl": "https://listen.mithraic.cloud",
|
||||||
"expectedStatuses": ["200-399"]
|
"expectedStatuses": ["200-399"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Voting Hub",
|
"name": "Voting Hub",
|
||||||
|
"group": "Main Services",
|
||||||
"url": "https://vote.mithraic.cloud",
|
"url": "https://vote.mithraic.cloud",
|
||||||
"displayUrl": "https://vote.mithraic.cloud",
|
"displayUrl": "https://vote.mithraic.cloud",
|
||||||
"expectedStatuses": ["200-399"]
|
"expectedStatuses": ["200-399"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Forgejo",
|
"name": "Forgejo",
|
||||||
|
"group": "Indexers & Search",
|
||||||
"url": "https://git.mithraic.cloud",
|
"url": "https://git.mithraic.cloud",
|
||||||
"displayUrl": "https://git.mithraic.cloud",
|
"displayUrl": "https://git.mithraic.cloud",
|
||||||
"expectedStatuses": ["200-399"]
|
"expectedStatuses": ["200-399"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Support",
|
"name": "Support",
|
||||||
|
"group": "Indexers & Search",
|
||||||
"url": "https://support.mithraic.cloud",
|
"url": "https://support.mithraic.cloud",
|
||||||
"displayUrl": "https://support.mithraic.cloud",
|
"displayUrl": "https://support.mithraic.cloud",
|
||||||
"expectedStatuses": ["200-399"]
|
"expectedStatuses": ["200-399"]
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ PBKDF2_ITERATIONS = 390_000
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Service:
|
class Service:
|
||||||
name: str
|
name: str
|
||||||
|
group: str
|
||||||
url: str
|
url: str
|
||||||
display_url: str
|
display_url: str
|
||||||
method: str
|
method: str
|
||||||
|
|
@ -337,6 +338,7 @@ def services_from_data(data: dict[str, Any]) -> list[Service]:
|
||||||
services.append(
|
services.append(
|
||||||
Service(
|
Service(
|
||||||
name=name,
|
name=name,
|
||||||
|
group=str(item.get("group", "Main Services")).strip() or "Main Services",
|
||||||
url=url,
|
url=url,
|
||||||
display_url=str(item.get("displayUrl", url)).strip() or url,
|
display_url=str(item.get("displayUrl", url)).strip() or url,
|
||||||
method=str(item.get("method", "GET")).strip().upper(),
|
method=str(item.get("method", "GET")).strip().upper(),
|
||||||
|
|
@ -379,6 +381,7 @@ def services_to_jsonable(services: list[Service]) -> list[dict[str, Any]]:
|
||||||
expected.append(f"{service.expected_min}-{service.expected_max}")
|
expected.append(f"{service.expected_min}-{service.expected_max}")
|
||||||
item: dict[str, Any] = {
|
item: dict[str, Any] = {
|
||||||
"name": service.name,
|
"name": service.name,
|
||||||
|
"group": service.group,
|
||||||
"url": service.url,
|
"url": service.url,
|
||||||
"displayUrl": service.display_url,
|
"displayUrl": service.display_url,
|
||||||
"method": service.method,
|
"method": service.method,
|
||||||
|
|
@ -502,7 +505,10 @@ def render_embeds(results: list[CheckResult]) -> list[dict[str, Any]]:
|
||||||
total = len(results)
|
total = len(results)
|
||||||
degraded = 0 < online < total
|
degraded = 0 < online < total
|
||||||
|
|
||||||
if online == total:
|
if total == 0:
|
||||||
|
color = 0x6B7280
|
||||||
|
summary = "No services configured."
|
||||||
|
elif online == total:
|
||||||
color = 0x10B981
|
color = 0x10B981
|
||||||
summary = f"🟢 Operational · {online}/{total} online"
|
summary = f"🟢 Operational · {online}/{total} online"
|
||||||
elif degraded:
|
elif degraded:
|
||||||
|
|
@ -514,33 +520,46 @@ def render_embeds(results: list[CheckResult]) -> list[dict[str, Any]]:
|
||||||
color = 0xEF4444
|
color = 0xEF4444
|
||||||
summary = f"🔴 Outage · {online}/{total} online"
|
summary = f"🔴 Outage · {online}/{total} online"
|
||||||
|
|
||||||
|
groups: dict[str, list[CheckResult]] = {}
|
||||||
|
for result in results:
|
||||||
|
groups.setdefault(result.service.group, []).append(result)
|
||||||
|
|
||||||
|
fields = []
|
||||||
|
for group_name, group_results in groups.items():
|
||||||
service_lines = []
|
service_lines = []
|
||||||
state_lines = []
|
state_lines = []
|
||||||
for result in results:
|
for result in group_results:
|
||||||
icon = "🟢" if result.ok else "🔴"
|
icon = "🟢" if result.ok else "🔴"
|
||||||
label = "Online" if result.ok else "Issue"
|
label = "Online" if result.ok else "Issue"
|
||||||
|
|
||||||
service_lines.append(f"**{result.service.name}**")
|
service_lines.append(f"**{result.service.name}**")
|
||||||
state_lines.append(f"{icon} {label}")
|
state_lines.append(f"{icon} {label}")
|
||||||
|
|
||||||
|
fields.append({
|
||||||
|
"name": group_name,
|
||||||
|
"value": "\n".join(service_lines)[:1024] or "None",
|
||||||
|
"inline": True,
|
||||||
|
})
|
||||||
|
fields.append({
|
||||||
|
"name": "Status",
|
||||||
|
"value": "\n".join(state_lines)[:1024] or "None",
|
||||||
|
"inline": True,
|
||||||
|
})
|
||||||
|
fields.append({
|
||||||
|
"name": "\u200b",
|
||||||
|
"value": "\u200b",
|
||||||
|
"inline": False,
|
||||||
|
})
|
||||||
|
|
||||||
|
if fields and fields[-1]["name"] == "\u200b":
|
||||||
|
fields.pop()
|
||||||
|
|
||||||
interval = os.getenv("CHECK_INTERVAL_SECONDS", str(DEFAULT_INTERVAL_SECONDS)).strip()
|
interval = os.getenv("CHECK_INTERVAL_SECONDS", str(DEFAULT_INTERVAL_SECONDS)).strip()
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"title": "The Mithral Archive",
|
"title": "The Mithral Archive",
|
||||||
"description": summary,
|
"description": summary,
|
||||||
"color": color,
|
"color": color,
|
||||||
"fields": [
|
"fields": fields[:25],
|
||||||
{
|
|
||||||
"name": "Service",
|
|
||||||
"value": "\n".join(service_lines)[:1024] or "No services configured.",
|
|
||||||
"inline": True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Status",
|
|
||||||
"value": "\n".join(state_lines)[:1024] or "n/a",
|
|
||||||
"inline": True,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"footer": {"text": f"Refreshes every {interval}s • Last checked {checked_at.strftime('%Y-%m-%d %H:%M:%S')} UTC"},
|
"footer": {"text": f"Refreshes every {interval}s • Last checked {checked_at.strftime('%Y-%m-%d %H:%M:%S')} UTC"},
|
||||||
"timestamp": checked_at.isoformat(),
|
"timestamp": checked_at.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
@ -596,6 +615,7 @@ def print_preview(services: list[Service]) -> None:
|
||||||
def result_to_jsonable(result: CheckResult) -> dict[str, Any]:
|
def result_to_jsonable(result: CheckResult) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"name": result.service.name,
|
"name": result.service.name,
|
||||||
|
"group": result.service.group,
|
||||||
"url": result.service.url,
|
"url": result.service.url,
|
||||||
"displayUrl": result.service.display_url,
|
"displayUrl": result.service.display_url,
|
||||||
"ok": result.ok,
|
"ok": result.ok,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue