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 {
|
||||
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;
|
||||
align-items: end;
|
||||
background: var(--panel);
|
||||
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 {
|
||||
|
|
@ -426,6 +447,7 @@
|
|||
function normalizeService(service = {}) {
|
||||
return {
|
||||
name: service.name || "",
|
||||
group: service.group || "Main Services",
|
||||
url: service.url || "",
|
||||
displayUrl: service.displayUrl || service.url || "",
|
||||
method: service.method || "GET",
|
||||
|
|
@ -454,8 +476,14 @@
|
|||
const row = document.createElement("div");
|
||||
row.className = "service-row";
|
||||
row.dataset.index = String(index);
|
||||
row.draggable = true;
|
||||
|
||||
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">
|
||||
<label>Name</label>
|
||||
<input data-key="name" value="${escapeAttr(service.name)}">
|
||||
|
|
@ -479,7 +507,7 @@
|
|||
<div class="row-status">
|
||||
${renderResult(result)}
|
||||
</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;
|
||||
|
|
@ -636,6 +664,44 @@
|
|||
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) => {
|
||||
if (error.status === 401) {
|
||||
showLogin();
|
||||
|
|
|
|||
15
preview.html
15
preview.html
|
|
@ -165,6 +165,14 @@
|
|||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.embed-field {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.embed-field.inline {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.embed-field-name {
|
||||
color: #f2f3f5;
|
||||
font-weight: 650;
|
||||
|
|
@ -320,7 +328,9 @@
|
|||
.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) {
|
||||
|
|
@ -350,6 +360,9 @@
|
|||
embed.fields.forEach((field) => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "embed-field";
|
||||
if (field.inline) {
|
||||
item.classList.add("inline");
|
||||
}
|
||||
const name = document.createElement("div");
|
||||
name.className = "embed-field-name";
|
||||
name.textContent = field.name || "\u200b";
|
||||
|
|
|
|||
|
|
@ -2,36 +2,42 @@
|
|||
"services": [
|
||||
{
|
||||
"name": "Portal",
|
||||
"group": "Main Services",
|
||||
"url": "https://mithraic.cloud",
|
||||
"displayUrl": "https://mithraic.cloud",
|
||||
"expectedStatuses": ["200-399"]
|
||||
},
|
||||
{
|
||||
"name": "Jellyfin",
|
||||
"group": "Main Services",
|
||||
"url": "https://jellyfin.mithraic.cloud/health",
|
||||
"displayUrl": "https://jellyfin.mithraic.cloud",
|
||||
"expectedStatuses": ["200-399"]
|
||||
},
|
||||
{
|
||||
"name": "Navidrome",
|
||||
"group": "Main Services",
|
||||
"url": "https://listen.mithraic.cloud",
|
||||
"displayUrl": "https://listen.mithraic.cloud",
|
||||
"expectedStatuses": ["200-399"]
|
||||
},
|
||||
{
|
||||
"name": "Voting Hub",
|
||||
"group": "Main Services",
|
||||
"url": "https://vote.mithraic.cloud",
|
||||
"displayUrl": "https://vote.mithraic.cloud",
|
||||
"expectedStatuses": ["200-399"]
|
||||
},
|
||||
{
|
||||
"name": "Forgejo",
|
||||
"group": "Indexers & Search",
|
||||
"url": "https://git.mithraic.cloud",
|
||||
"displayUrl": "https://git.mithraic.cloud",
|
||||
"expectedStatuses": ["200-399"]
|
||||
},
|
||||
{
|
||||
"name": "Support",
|
||||
"group": "Indexers & Search",
|
||||
"url": "https://support.mithraic.cloud",
|
||||
"displayUrl": "https://support.mithraic.cloud",
|
||||
"expectedStatuses": ["200-399"]
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ PBKDF2_ITERATIONS = 390_000
|
|||
@dataclass(frozen=True)
|
||||
class Service:
|
||||
name: str
|
||||
group: str
|
||||
url: str
|
||||
display_url: str
|
||||
method: str
|
||||
|
|
@ -337,6 +338,7 @@ def services_from_data(data: dict[str, Any]) -> list[Service]:
|
|||
services.append(
|
||||
Service(
|
||||
name=name,
|
||||
group=str(item.get("group", "Main Services")).strip() or "Main Services",
|
||||
url=url,
|
||||
display_url=str(item.get("displayUrl", url)).strip() or url,
|
||||
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}")
|
||||
item: dict[str, Any] = {
|
||||
"name": service.name,
|
||||
"group": service.group,
|
||||
"url": service.url,
|
||||
"displayUrl": service.display_url,
|
||||
"method": service.method,
|
||||
|
|
@ -502,7 +505,10 @@ def render_embeds(results: list[CheckResult]) -> list[dict[str, Any]]:
|
|||
total = len(results)
|
||||
degraded = 0 < online < total
|
||||
|
||||
if online == total:
|
||||
if total == 0:
|
||||
color = 0x6B7280
|
||||
summary = "No services configured."
|
||||
elif online == total:
|
||||
color = 0x10B981
|
||||
summary = f"🟢 Operational · {online}/{total} online"
|
||||
elif degraded:
|
||||
|
|
@ -514,33 +520,46 @@ def render_embeds(results: list[CheckResult]) -> list[dict[str, Any]]:
|
|||
color = 0xEF4444
|
||||
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 = []
|
||||
state_lines = []
|
||||
for result in results:
|
||||
for result in group_results:
|
||||
icon = "🟢" if result.ok else "🔴"
|
||||
label = "Online" if result.ok else "Issue"
|
||||
|
||||
service_lines.append(f"**{result.service.name}**")
|
||||
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()
|
||||
return [
|
||||
{
|
||||
"title": "The Mithral Archive",
|
||||
"description": summary,
|
||||
"color": color,
|
||||
"fields": [
|
||||
{
|
||||
"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,
|
||||
}
|
||||
],
|
||||
"fields": fields[:25],
|
||||
"footer": {"text": f"Refreshes every {interval}s • Last checked {checked_at.strftime('%Y-%m-%d %H:%M:%S')} UTC"},
|
||||
"timestamp": checked_at.isoformat(),
|
||||
}
|
||||
|
|
@ -596,6 +615,7 @@ def print_preview(services: list[Service]) -> None:
|
|||
def result_to_jsonable(result: CheckResult) -> dict[str, Any]:
|
||||
return {
|
||||
"name": result.service.name,
|
||||
"group": result.service.group,
|
||||
"url": result.service.url,
|
||||
"displayUrl": result.service.display_url,
|
||||
"ok": result.ok,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue