feat: improve status page

This commit is contained in:
2026-04-08 16:26:01 +04:00
parent 60037edf21
commit 601c918e9f
7 changed files with 408 additions and 47 deletions
+31 -3
View File
@@ -636,7 +636,7 @@ export const dashboard = new Elysia()
`;
if (!page) return redirect("/dashboard/status-pages");
const monitors = await sql`
SELECT monitor_id FROM status_page_monitors WHERE status_page_id = ${params.id}
SELECT monitor_id, display_name FROM status_page_monitors WHERE status_page_id = ${params.id}
`;
const allMonitors = await sql`
SELECT id, name FROM monitors WHERE account_id = ${resolved.accountId} ORDER BY created_at DESC
@@ -650,6 +650,21 @@ export const dashboard = new Elysia()
if (!resolved?.accountId) return redirect("/dashboard");
const b = body as any;
const monitorIds = Array.isArray(b.monitor_ids) ? b.monitor_ids : (b.monitor_ids ? [b.monitor_ids] : []);
// The form posts per-monitor display name overrides as display_name[<id>].
// Bun's body parser surfaces them as nested object keys (display_name) or
// as flat "display_name[id]" string keys depending on parser version, so
// handle both. Empty strings mean "no override" — don't send them.
const displayNames: Record<string, string> = {};
if (b.display_name && typeof b.display_name === "object" && !Array.isArray(b.display_name)) {
for (const [k, v] of Object.entries(b.display_name)) {
if (typeof v === "string" && v.trim()) displayNames[k] = v.trim();
}
} else {
for (const k of Object.keys(b)) {
const m = k.match(/^display_name\[(.+)\]$/);
if (m && typeof b[k] === "string" && b[k].trim()) displayNames[m[1]!] = b[k].trim();
}
}
try {
const apiUrl = process.env.API_URL || "https://api.pingql.com";
const key = cookie?.pingql_key?.value;
@@ -662,13 +677,14 @@ export const dashboard = new Elysia()
description: b.description || null,
theme: b.theme || "auto",
default_window: b.default_window || "24h",
display_mode: b.display_mode || "expanded",
show_response_time: !!b.show_response_time,
show_powered_by: !!b.show_powered_by,
index_search: !!b.index_search,
password: b.password || undefined,
custom_css: b.custom_css || null,
footer_text: b.footer_text || null,
monitors: monitorIds.map((id: string, i: number) => ({ monitor_id: id, position: i })),
monitors: monitorIds.map((id: string, i: number) => ({ monitor_id: id, position: i, display_name: displayNames[id] ?? null })),
}),
});
} catch {}
@@ -680,6 +696,17 @@ export const dashboard = new Elysia()
if (!resolved?.accountId) return redirect("/dashboard");
const b = body as any;
const monitorIds = Array.isArray(b.monitor_ids) ? b.monitor_ids : (b.monitor_ids ? [b.monitor_ids] : []);
const displayNames: Record<string, string> = {};
if (b.display_name && typeof b.display_name === "object" && !Array.isArray(b.display_name)) {
for (const [k, v] of Object.entries(b.display_name)) {
if (typeof v === "string" && v.trim()) displayNames[k] = v.trim();
}
} else {
for (const k of Object.keys(b)) {
const m = k.match(/^display_name\[(.+)\]$/);
if (m && typeof b[k] === "string" && b[k].trim()) displayNames[m[1]!] = b[k].trim();
}
}
try {
const apiUrl = process.env.API_URL || "https://api.pingql.com";
const key = cookie?.pingql_key?.value;
@@ -689,12 +716,13 @@ export const dashboard = new Elysia()
description: b.description || null,
theme: b.theme || "auto",
default_window: b.default_window || "24h",
display_mode: b.display_mode || "expanded",
show_response_time: !!b.show_response_time,
show_powered_by: !!b.show_powered_by,
index_search: !!b.index_search,
custom_css: b.custom_css || null,
footer_text: b.footer_text || null,
monitors: monitorIds.map((id: string, i: number) => ({ monitor_id: id, position: i })),
monitors: monitorIds.map((id: string, i: number) => ({ monitor_id: id, position: i, display_name: displayNames[id] ?? null })),
};
// Only send `password` if the user actually typed something. An empty box
// means "leave the existing password as-is" — sending null would clear it.
+27 -7
View File
@@ -4,7 +4,11 @@
<%
const p = it.page || {};
const allMonitors = it.allMonitors || [];
const attached = new Set((it.page?.monitors || []).map(m => m.monitor_id));
const attachedRows = (it.page?.monitors || []);
const attached = new Set(attachedRows.map(m => m.monitor_id));
// monitor_id → existing display_name override (or empty)
const displayNames = {};
for (const r of attachedRows) displayNames[r.monitor_id] = r.display_name || '';
%>
<main class="max-w-3xl mx-auto px-8 py-10">
@@ -52,19 +56,35 @@
<% }) %>
</select>
</div>
<div class="flex-1">
<label class="block text-sm text-gray-400 mb-1.5">Display mode</label>
<select name="display_mode" class="w-full bg-surface-solid border border-border-subtle rounded-lg px-4 py-2.5 text-gray-100 focus:outline-none focus:border-blue-500">
<% [['expanded','Expanded — show full detail per monitor'],['compact','Compact — one line per monitor, click to expand']].forEach(function([v, label]) { %>
<option value="<%= v %>" <%= (p.display_mode || 'expanded') === v ? 'selected' : '' %>><%= label %></option>
<% }) %>
</select>
</div>
</div>
<div>
<label class="block text-sm text-gray-400 mb-1.5">Monitors</label>
<p class="text-xs text-gray-600 mb-2">Optional "Show as" field overrides the monitor name on this status page only. Leave blank to use the monitor's real name.</p>
<% if (allMonitors.length === 0) { %>
<p class="text-xs text-gray-600">No monitors yet. <a href="/dashboard/monitors/new" class="text-blue-400 hover:text-blue-300">Create one</a> first.</p>
<% } else { %>
<div class="flex flex-wrap gap-2">
<% allMonitors.forEach(function(m) { %>
<label class="flex items-center gap-2 bg-gray-900 border border-gray-800 hover:border-gray-600 rounded-lg px-3 py-2 cursor-pointer transition-colors">
<input type="checkbox" name="monitor_ids" value="<%= m.id %>" class="accent-blue-500" <%= attached.has(m.id) ? 'checked' : '' %>>
<span class="text-sm text-gray-300"><%= m.name %></span>
</label>
<div class="space-y-2">
<% allMonitors.forEach(function(m) {
const isAttached = attached.has(m.id);
const displayName = displayNames[m.id] || '';
%>
<div class="flex items-center gap-3 bg-gray-900 border border-gray-800 rounded-lg px-3 py-2">
<label class="flex items-center gap-2 cursor-pointer min-w-0 flex-1">
<input type="checkbox" name="monitor_ids" value="<%= m.id %>" class="accent-blue-500" <%= isAttached ? 'checked' : '' %>>
<span class="text-sm text-gray-300 truncate"><%= m.name %></span>
</label>
<input type="text" name="display_name[<%= m.id %>]" value="<%= displayName %>" placeholder="Show as (optional)"
class="text-xs bg-gray-950 border border-gray-800 rounded px-2 py-1 text-gray-200 placeholder-gray-600 focus:outline-none focus:border-blue-500 w-48 shrink-0">
</div>
<% }) %>
</div>
<% } %>