del
This commit is contained in:
+149
-98
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Dashboard</title>
|
<title>Dashboard</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
@@ -52,13 +52,25 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.green { background: #3ad29f; }
|
.green {
|
||||||
.yellow { background: #f5c542; }
|
background: #3ad29f;
|
||||||
.red { background: #e05d5d; }
|
}
|
||||||
|
.yellow {
|
||||||
|
background: #f5c542;
|
||||||
|
}
|
||||||
|
.red {
|
||||||
|
background: #e05d5d;
|
||||||
|
}
|
||||||
|
|
||||||
.green-text { color: #3ad29f; }
|
.green-text {
|
||||||
.yellow-text { color: #f5c542; }
|
color: #3ad29f;
|
||||||
.red-text { color: #e05d5d; }
|
}
|
||||||
|
.yellow-text {
|
||||||
|
color: #f5c542;
|
||||||
|
}
|
||||||
|
.red-text {
|
||||||
|
color: #e05d5d;
|
||||||
|
}
|
||||||
|
|
||||||
/* SERVER GRID */
|
/* SERVER GRID */
|
||||||
.servers {
|
.servers {
|
||||||
@@ -146,8 +158,14 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-odoo { background: #714b67; color: #e6edf3; }
|
.badge-odoo {
|
||||||
.badge-brandize { background: #1a6fa0; color: #e6edf3; }
|
background: #714b67;
|
||||||
|
color: #e6edf3;
|
||||||
|
}
|
||||||
|
.badge-brandize {
|
||||||
|
background: #1a6fa0;
|
||||||
|
color: #e6edf3;
|
||||||
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -298,49 +316,65 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="dashboard" id="dashboard">
|
<div class="dashboard" id="dashboard">
|
||||||
<div class="loading" style="grid-column:1/-1;grid-row:1/-1;">Loading dashboard…</div>
|
<div class="loading" style="grid-column: 1/-1; grid-row: 1/-1">
|
||||||
|
Loading dashboard…
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const WEBHOOK_URL = 'https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/52781c84-9f12-4a61-96b2-9e1e3933148d';
|
const WEBHOOK_URL =
|
||||||
|
"https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/52781c84-9f12-4a61-96b2-9e1e3933148d";
|
||||||
const REFRESH_INTERVAL_MS = 60 * 1000; // refresh every 60 seconds
|
const REFRESH_INTERVAL_MS = 60 * 1000; // refresh every 60 seconds
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function mbToGb(mb) { return (mb / 1024).toFixed(1); }
|
function mbToGb(mb) {
|
||||||
function pct(free, total) { return total > 0 ? (free / total * 100) : 0; }
|
return (mb / 1024).toFixed(1);
|
||||||
|
}
|
||||||
|
function pct(free, total) {
|
||||||
|
return total > 0 ? (free / total) * 100 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
function metricColor(freePct) {
|
function metricColor(freePct) {
|
||||||
if (freePct < 10) return 'red';
|
if (freePct < 10) return "red";
|
||||||
if (freePct < 20) return 'yellow';
|
if (freePct < 20) return "yellow";
|
||||||
return 'green';
|
return "green";
|
||||||
}
|
}
|
||||||
|
|
||||||
function worstColor(colors) {
|
function worstColor(colors) {
|
||||||
if (colors.includes('red')) return 'red';
|
if (colors.includes("red")) return "red";
|
||||||
if (colors.includes('yellow')) return 'yellow';
|
if (colors.includes("yellow")) return "yellow";
|
||||||
return 'green';
|
return "green";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLocalTime(dateTimeStr) {
|
function formatLocalTime(dateTimeStr) {
|
||||||
const d = new Date(dateTimeStr);
|
const d = new Date(dateTimeStr);
|
||||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
|
return d.toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLocalDate(dateTimeStr) {
|
function formatLocalDate(dateTimeStr) {
|
||||||
const d = new Date(dateTimeStr);
|
const d = new Date(dateTimeStr);
|
||||||
return d.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' });
|
return d.toLocaleDateString([], {
|
||||||
|
weekday: "short",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isToday(dateTimeStr) {
|
function isToday(dateTimeStr) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const d = new Date(dateTimeStr);
|
const d = new Date(dateTimeStr);
|
||||||
return d.getFullYear() === now.getFullYear() &&
|
return (
|
||||||
|
d.getFullYear() === now.getFullYear() &&
|
||||||
d.getMonth() === now.getMonth() &&
|
d.getMonth() === now.getMonth() &&
|
||||||
d.getDate() === now.getDate();
|
d.getDate() === now.getDate()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSoonOrOngoing(startStr, endStr) {
|
function isSoonOrOngoing(startStr, endStr) {
|
||||||
@@ -364,14 +398,16 @@ function buildServerCard(s) {
|
|||||||
const lightColor = worstColor([ramColor, hddColor]);
|
const lightColor = worstColor([ramColor, hddColor]);
|
||||||
|
|
||||||
const displayNames = {
|
const displayNames = {
|
||||||
saopaulo: 'São Paulo',
|
saopaulo: "São Paulo",
|
||||||
manchester: 'Manchester',
|
manchester: "Manchester",
|
||||||
mumbai: 'Mumbai',
|
mumbai: "Mumbai",
|
||||||
sydney: 'Sydney',
|
sydney: "Sydney",
|
||||||
meppel: 'Meppel',
|
meppel: "Meppel",
|
||||||
boston: 'Boston'
|
boston: "Boston",
|
||||||
};
|
};
|
||||||
const displayName = displayNames[s.Server] || (s.Server.charAt(0).toUpperCase() + s.Server.slice(1));
|
const displayName =
|
||||||
|
displayNames[s.Server] ||
|
||||||
|
s.Server.charAt(0).toUpperCase() + s.Server.slice(1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lightColor,
|
lightColor,
|
||||||
@@ -390,7 +426,7 @@ function buildServerCard(s) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,29 +446,39 @@ function parseMeetings(calEvents, calName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildMeetingCard(m) {
|
function buildMeetingCard(m) {
|
||||||
const startStr = (m.start && (m.start.dateTime || m.start.date)) || '';
|
const startStr =
|
||||||
const endStr = (m.end && (m.end.dateTime || m.end.date)) || '';
|
(m.start && (m.start.dateTime || m.start.date)) || "";
|
||||||
const soon = startStr && endStr ? isSoonOrOngoing(startStr, endStr) : false;
|
const endStr = (m.end && (m.end.dateTime || m.end.date)) || "";
|
||||||
|
const soon =
|
||||||
|
startStr && endStr
|
||||||
|
? isSoonOrOngoing(startStr, endStr)
|
||||||
|
: false;
|
||||||
const today = startStr ? isToday(startStr) : false;
|
const today = startStr ? isToday(startStr) : false;
|
||||||
|
|
||||||
const badges = (m._cals || []).map(c => {
|
const badges = (m._cals || [])
|
||||||
if (c === 'ODOO4projects') return `<span class="badge badge-odoo">ODOO4</span>`;
|
.map((c) => {
|
||||||
if (c === 'Brandize') return `<span class="badge badge-brandize">Brandize</span>`;
|
if (c === "ODOO4projects")
|
||||||
return '';
|
return `<span class="badge badge-odoo">ODOO4</span>`;
|
||||||
}).join('');
|
if (c === "Brandize")
|
||||||
|
return `<span class="badge badge-brandize">Brandize</span>`;
|
||||||
|
return "";
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
const dateLabel = today || !startStr
|
const dateLabel =
|
||||||
? ''
|
today || !startStr
|
||||||
|
? ""
|
||||||
: `<span style="font-size:10px;opacity:0.55;margin-right:4px;">${formatLocalDate(startStr)}</span>`;
|
: `<span style="font-size:10px;opacity:0.55;margin-right:4px;">${formatLocalDate(startStr)}</span>`;
|
||||||
|
|
||||||
const timeLabel = startStr && endStr
|
const timeLabel =
|
||||||
|
startStr && endStr
|
||||||
? `${formatLocalTime(startStr)} – ${formatLocalTime(endStr)}`
|
? `${formatLocalTime(startStr)} – ${formatLocalTime(endStr)}`
|
||||||
: '';
|
: "";
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="meeting${soon ? ' soon' : ''}">
|
<div class="meeting${soon ? " soon" : ""}">
|
||||||
<div class="meeting-header">
|
<div class="meeting-header">
|
||||||
<div class="meeting-name">${m.summary || 'Meeting'}</div>
|
<div class="meeting-name">${m.summary || "Meeting"}</div>
|
||||||
<div class="meeting-badges">${badges}</div>
|
<div class="meeting-badges">${badges}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="time">${dateLabel}${timeLabel}</div>
|
<div class="time">${dateLabel}${timeLabel}</div>
|
||||||
@@ -447,11 +493,11 @@ function buildTodoItem(t) {
|
|||||||
const done = !!t.done;
|
const done = !!t.done;
|
||||||
const checkInner = done
|
const checkInner = done
|
||||||
? `<svg width="10" height="10" viewBox="0 0 10 10"><polyline points="1.5,5 4,7.5 8.5,2.5" stroke="#fff" stroke-width="1.8" fill="none" stroke-linecap="round"/></svg>`
|
? `<svg width="10" height="10" viewBox="0 0 10 10"><polyline points="1.5,5 4,7.5 8.5,2.5" stroke="#fff" stroke-width="1.8" fill="none" stroke-linecap="round"/></svg>`
|
||||||
: '';
|
: "";
|
||||||
return `
|
return `
|
||||||
<div class="todo${done ? ' done-todo' : ''}">
|
<div class="todo${done ? " done-todo" : ""}">
|
||||||
<div class="todo-check${done ? ' checked' : ''}">${checkInner}</div>
|
<div class="todo-check${done ? " checked" : ""}">${checkInner}</div>
|
||||||
<span>${t.Todotodo || ''}</span>
|
<span>${t.Todotodo || ""}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -460,19 +506,23 @@ function buildTodoItem(t) {
|
|||||||
// Latest Signup item
|
// Latest Signup item
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function buildSignupItem(ct) {
|
function buildSignupItem(ct) {
|
||||||
if (!ct) return '';
|
if (!ct) return "";
|
||||||
const tagBadges = (ct.tags || '').split(',').map(t => t.trim()).filter(Boolean)
|
const tagBadges = (ct.tags || "")
|
||||||
.map(t => `<span class="signup-tag">${t}</span>`).join('');
|
.split(",")
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((t) => `<span class="signup-tag">${t}</span>`)
|
||||||
|
.join("");
|
||||||
return `
|
return `
|
||||||
<div class="signup-item">
|
<div class="signup-item">
|
||||||
<div class="signup-email">${ct.email || ''}</div>
|
<div class="signup-email">${ct.email || ""}</div>
|
||||||
<div class="signup-meta">
|
<div class="signup-meta">
|
||||||
<span class="signup-server">🖥 ${ct.server || ''}</span>
|
<span class="signup-server">🖥 ${ct.server || ""}</span>
|
||||||
${tagBadges}
|
${tagBadges}
|
||||||
<span style="opacity:0.5;font-size:10px;">${ct.status || ''}</span>
|
<span style="opacity:0.5;font-size:10px;">${ct.status || ""}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:4px;opacity:0.55;font-size:10px;">
|
<div style="margin-top:4px;opacity:0.55;font-size:10px;">
|
||||||
${ct.Domains ? ct.Domains : ''}${ct.expires ? ' · expires ' + ct.expires : ''}
|
${ct.Domains ? ct.Domains : ""}${ct.expires ? " · expires " + ct.expires : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -482,11 +532,11 @@ function buildSignupItem(ct) {
|
|||||||
// Render
|
// Render
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function render(data) {
|
function render(data) {
|
||||||
const serverStatusRaw = data['Server Status'] || [];
|
const serverStatusRaw = data["Server Status"] || [];
|
||||||
const calOdoo = data['Calender ODOO4projects'] || [];
|
const calOdoo = data["Calender ODOO4projects"] || [];
|
||||||
const calBrandize = data['Calender Brandize'] || [];
|
const calBrandize = data["Calender Brandize"] || [];
|
||||||
const todos = data['Todos'] || [];
|
const todos = data["Todos"] || [];
|
||||||
const currentTest = data['Current Tests'] || null;
|
const currentTest = data["Current Tests"] || null;
|
||||||
|
|
||||||
// Deduplicate servers by Server key
|
// Deduplicate servers by Server key
|
||||||
const serverMap = {};
|
const serverMap = {};
|
||||||
@@ -497,12 +547,14 @@ function render(data) {
|
|||||||
|
|
||||||
// Server cards
|
// Server cards
|
||||||
const serverCards = servers.map(buildServerCard);
|
const serverCards = servers.map(buildServerCard);
|
||||||
const overallColor = worstColor(serverCards.map(c => c.lightColor));
|
const overallColor = worstColor(
|
||||||
const serverCardsHtml = serverCards.map(c => c.html).join('');
|
serverCards.map((c) => c.lightColor),
|
||||||
|
);
|
||||||
|
const serverCardsHtml = serverCards.map((c) => c.html).join("");
|
||||||
|
|
||||||
// Meetings – merge & deduplicate across both calendars
|
// Meetings – merge & deduplicate across both calendars
|
||||||
const odooMeetings = parseMeetings(calOdoo, 'ODOO4projects');
|
const odooMeetings = parseMeetings(calOdoo, "ODOO4projects");
|
||||||
const brandizeMeetings = parseMeetings(calBrandize, 'Brandize');
|
const brandizeMeetings = parseMeetings(calBrandize, "Brandize");
|
||||||
const allMeetings = [...odooMeetings, ...brandizeMeetings];
|
const allMeetings = [...odooMeetings, ...brandizeMeetings];
|
||||||
|
|
||||||
const meetingById = {};
|
const meetingById = {};
|
||||||
@@ -514,17 +566,25 @@ function render(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dedupedMeetings = Object.values(meetingById).sort((a, b) => {
|
const dedupedMeetings = Object.values(meetingById).sort(
|
||||||
const aStart = (a.start && (a.start.dateTime || a.start.date)) || '';
|
(a, b) => {
|
||||||
const bStart = (b.start && (b.start.dateTime || b.start.date)) || '';
|
const aStart =
|
||||||
|
(a.start && (a.start.dateTime || a.start.date)) ||
|
||||||
|
"";
|
||||||
|
const bStart =
|
||||||
|
(b.start && (b.start.dateTime || b.start.date)) ||
|
||||||
|
"";
|
||||||
return new Date(aStart) - new Date(bStart);
|
return new Date(aStart) - new Date(bStart);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const meetingsHtml = dedupedMeetings.map(buildMeetingCard).join('');
|
const meetingsHtml = dedupedMeetings
|
||||||
const todosHtml = todos.map(buildTodoItem).join('');
|
.map(buildMeetingCard)
|
||||||
|
.join("");
|
||||||
|
const todosHtml = todos.map(buildTodoItem).join("");
|
||||||
const signupsHtml = buildSignupItem(currentTest);
|
const signupsHtml = buildSignupItem(currentTest);
|
||||||
|
|
||||||
document.getElementById('dashboard').innerHTML = `
|
document.getElementById("dashboard").innerHTML = `
|
||||||
|
|
||||||
<!-- SERVER STATUS -->
|
<!-- SERVER STATUS -->
|
||||||
<div class="box" style="grid-column:1; grid-row:1/3;">
|
<div class="box" style="grid-column:1; grid-row:1/3;">
|
||||||
@@ -562,18 +622,22 @@ function render(data) {
|
|||||||
<!-- CURRENT TESTS -->
|
<!-- CURRENT TESTS -->
|
||||||
<div class="box" style="grid-column:3; grid-row:2;">
|
<div class="box" style="grid-column:3; grid-row:2;">
|
||||||
<div class="title">Current Test</div>
|
<div class="title">Current Test</div>
|
||||||
${currentTest ? `
|
${
|
||||||
|
currentTest
|
||||||
|
? `
|
||||||
<div class="test-box">
|
<div class="test-box">
|
||||||
<div class="test-row"><span class="test-label">UUID</span><span class="test-value" style="font-size:10px;">${currentTest.UUID || ''}</span></div>
|
<div class="test-row"><span class="test-label">UUID</span><span class="test-value" style="font-size:10px;">${currentTest.UUID || ""}</span></div>
|
||||||
<div class="test-row"><span class="test-label">Email</span><span class="test-value">${currentTest.email || ''}</span></div>
|
<div class="test-row"><span class="test-label">Email</span><span class="test-value">${currentTest.email || ""}</span></div>
|
||||||
<div class="test-row"><span class="test-label">Server</span><span class="test-value">${currentTest.server || ''}</span></div>
|
<div class="test-row"><span class="test-label">Server</span><span class="test-value">${currentTest.server || ""}</span></div>
|
||||||
<div class="test-row"><span class="test-label">Tags</span><span class="test-value"><span class="signup-tag">${currentTest.tags || ''}</span></span></div>
|
<div class="test-row"><span class="test-label">Tags</span><span class="test-value"><span class="signup-tag">${currentTest.tags || ""}</span></span></div>
|
||||||
<div class="test-row"><span class="test-label">Status</span><span class="test-value"><span class="status-badge dev">${currentTest.status || ''}</span></span></div>
|
<div class="test-row"><span class="test-label">Status</span><span class="test-value"><span class="status-badge dev">${currentTest.status || ""}</span></span></div>
|
||||||
<div class="test-row"><span class="test-label">Expires</span><span class="test-value">${currentTest.expires || ''}</span></div>
|
<div class="test-row"><span class="test-label">Expires</span><span class="test-value">${currentTest.expires || ""}</span></div>
|
||||||
<div class="test-row"><span class="test-label">Domains</span><span class="test-value" style="font-size:10px;">${currentTest.Domains || ''}</span></div>
|
<div class="test-row"><span class="test-label">Domains</span><span class="test-value" style="font-size:10px;">${currentTest.Domains || ""}</span></div>
|
||||||
<div class="test-row"><span class="test-label">Created</span><span class="test-value">${currentTest.created || ''}</span></div>
|
<div class="test-row"><span class="test-label">Created</span><span class="test-value">${currentTest.created || ""}</span></div>
|
||||||
</div>
|
</div>
|
||||||
` : '<div style="opacity:0.4;font-size:13px;">No active test</div>'}
|
`
|
||||||
|
: '<div style="opacity:0.4;font-size:13px;">No active test</div>'
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
`;
|
`;
|
||||||
@@ -589,7 +653,7 @@ async function loadData() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
render(data);
|
render(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
document.getElementById('dashboard').innerHTML = `
|
document.getElementById("dashboard").innerHTML = `
|
||||||
<div class="error-msg" style="grid-column:1/-1;grid-row:1/-1;padding:40px;">
|
<div class="error-msg" style="grid-column:1/-1;grid-row:1/-1;padding:40px;">
|
||||||
⚠ Failed to load dashboard data: ${err.message}<br>
|
⚠ Failed to load dashboard data: ${err.message}<br>
|
||||||
<span style="font-size:11px;opacity:0.6;">Retrying in ${REFRESH_INTERVAL_MS / 1000}s…</span>
|
<span style="font-size:11px;opacity:0.6;">Retrying in ${REFRESH_INTERVAL_MS / 1000}s…</span>
|
||||||
@@ -603,16 +667,3 @@ setInterval(loadData, REFRESH_INTERVAL_MS);
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
```
|
|
||||||
|
|
||||||
Here's what changed from the original:
|
|
||||||
|
|
||||||
1. **Webhook fetch** — replaced the `{{ $json.info }}` n8n template variable with a proper `fetch()` call to the webhook URL. Data loads dynamically on page open.
|
|
||||||
|
|
||||||
2. **Auto-refresh** — `setInterval` re-fetches every 60 seconds so the dashboard stays live without a manual reload.
|
|
||||||
|
|
||||||
3. **Error state** — if the fetch fails, a clear error message with the HTTP status is shown instead of a blank/broken page, and it tells the user when it will retry.
|
|
||||||
|
|
||||||
4. **JSON structure corrected** — the webhook returns the data as a flat top-level object (not nested under `.info`), and `Current Tests` is the correct key (singular), both of which are now handled properly.
|
|
||||||
|
|
||||||
5. **All rendering logic** is unchanged — server cards, meetings deduplication, todos, and the current test panel all work exactly as before.
|
|
||||||
|
|||||||
-577
@@ -1,577 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Dashboard</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
width: 1920px;
|
|
||||||
height: 1200px;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background: #2f3e4e;
|
|
||||||
color: #e6edf3;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 2fr 1fr 1fr;
|
|
||||||
grid-template-rows: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box {
|
|
||||||
background: #3b4c5e;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 18px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
box-shadow: 0 6px 14px rgba(0,0,0,0.25);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-light {
|
|
||||||
position: absolute;
|
|
||||||
top: 18px;
|
|
||||||
right: 18px;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.green { background: #3ad29f; }
|
|
||||||
.yellow { background: #f5c542; }
|
|
||||||
.red { background: #e05d5d; }
|
|
||||||
|
|
||||||
.green-text { color: #3ad29f; }
|
|
||||||
.yellow-text { color: #f5c542; }
|
|
||||||
.red-text { color: #e05d5d; }
|
|
||||||
|
|
||||||
/* SERVER GRID */
|
|
||||||
.servers {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server {
|
|
||||||
background: #44586d;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-name {
|
|
||||||
font-size: 15px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
opacity: 0.85;
|
|
||||||
font-weight: bold;
|
|
||||||
padding-right: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server .status-light {
|
|
||||||
top: 12px;
|
|
||||||
right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metrics {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.7;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MEETINGS */
|
|
||||||
.meetings-scroll {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: calc(100% - 45px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting {
|
|
||||||
background: #44586d;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting.soon {
|
|
||||||
border: 2px solid #e05d5d;
|
|
||||||
box-shadow: 0 0 8px rgba(224,93,93,0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-name {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
opacity: 0.95;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-badges {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
font-size: 9px;
|
|
||||||
padding: 2px 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-odoo { background: #714b67; color: #e6edf3; }
|
|
||||||
.badge-brandize { background: #1a6fa0; color: #e6edf3; }
|
|
||||||
|
|
||||||
.time {
|
|
||||||
font-size: 11px;
|
|
||||||
opacity: 0.65;
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TODOS */
|
|
||||||
.todos-scroll {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: calc(100% - 45px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo {
|
|
||||||
background: #44586d;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.9;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo.done-todo {
|
|
||||||
opacity: 0.5;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo-check {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 2px solid #8aa0b5;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo-check.checked {
|
|
||||||
background: #3ad29f;
|
|
||||||
border-color: #3ad29f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LATEST SIGNUPS */
|
|
||||||
.signups-scroll {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: calc(100% - 45px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-item {
|
|
||||||
background: #44586d;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-email {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
opacity: 0.95;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
opacity: 0.7;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-tag {
|
|
||||||
background: #2f3e4e;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1px 6px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #3ad29f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-server {
|
|
||||||
font-size: 11px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CURRENT TESTS */
|
|
||||||
.test-box {
|
|
||||||
background: #44586d;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
||||||
padding: 3px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-label {
|
|
||||||
opacity: 0.6;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-value {
|
|
||||||
font-size: 11px;
|
|
||||||
text-align: right;
|
|
||||||
max-width: 65%;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 1px 7px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
background: #3ad29f33;
|
|
||||||
color: #3ad29f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.dev {
|
|
||||||
background: #f5c54233;
|
|
||||||
color: #f5c542;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="dashboard" id="dashboard"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const WEBHOOK_URL = 'https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/52781c84-9f12-4a61-96b2-9e1e3933148d';
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function mbToGb(mb) { return (mb / 1024).toFixed(1); }
|
|
||||||
function pct(free, total) { return total > 0 ? (free / total * 100) : 0; }
|
|
||||||
|
|
||||||
function metricColor(freePct) {
|
|
||||||
if (freePct < 10) return 'red';
|
|
||||||
if (freePct < 20) return 'yellow';
|
|
||||||
return 'green';
|
|
||||||
}
|
|
||||||
|
|
||||||
function worstColor(colors) {
|
|
||||||
if (colors.includes('red')) return 'red';
|
|
||||||
if (colors.includes('yellow')) return 'yellow';
|
|
||||||
return 'green';
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatLocalTime(dateTimeStr) {
|
|
||||||
return new Date(dateTimeStr).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatLocalDate(dateTimeStr) {
|
|
||||||
return new Date(dateTimeStr).toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function isToday(dateTimeStr) {
|
|
||||||
const now = new Date(), d = new Date(dateTimeStr);
|
|
||||||
return d.getFullYear() === now.getFullYear() &&
|
|
||||||
d.getMonth() === now.getMonth() &&
|
|
||||||
d.getDate() === now.getDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSoonOrOngoing(startStr, endStr) {
|
|
||||||
const now = new Date();
|
|
||||||
const start = new Date(startStr);
|
|
||||||
const end = new Date(endStr);
|
|
||||||
const diffMin = (start - now) / 60000;
|
|
||||||
if (now >= start && now <= end) return true;
|
|
||||||
if (diffMin >= 0 && diffMin <= 30) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Server cards
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function buildServerCard(s) {
|
|
||||||
const ramPct = pct(s.free_ram, s.available_ram);
|
|
||||||
const hddPct = pct(s.free_hdd, s.available_hdd);
|
|
||||||
const ramColor = metricColor(ramPct);
|
|
||||||
const hddColor = metricColor(hddPct);
|
|
||||||
const light = worstColor([ramColor, hddColor]);
|
|
||||||
|
|
||||||
const names = { saopaulo: 'São Paulo', manchester: 'Manchester', mumbai: 'Mumbai', sydney: 'Sydney', meppel: 'Meppel', boston: 'Boston' };
|
|
||||||
const displayName = names[s.Server] || (s.Server.charAt(0).toUpperCase() + s.Server.slice(1));
|
|
||||||
|
|
||||||
return {
|
|
||||||
light,
|
|
||||||
html: `
|
|
||||||
<div class="server">
|
|
||||||
<div class="server-name">${displayName}</div>
|
|
||||||
<div class="status-light ${light}"></div>
|
|
||||||
<div class="metrics">
|
|
||||||
<div class="metric-row"><span>RAM:</span><span class="${ramColor}-text">${ramPct.toFixed(0)}% free (${mbToGb(s.free_ram)} GB)</span></div>
|
|
||||||
<div class="metric-row"><span>HDD:</span><span class="${hddColor}-text">${hddPct.toFixed(0)}% free (${mbToGb(s.free_hdd)} GB)</span></div>
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Meetings
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function parseMeetings(calEvents, calName) {
|
|
||||||
const seen = new Set(), result = [];
|
|
||||||
for (const e of calEvents) {
|
|
||||||
const ev = e.json || e;
|
|
||||||
if (seen.has(ev.id)) continue;
|
|
||||||
seen.add(ev.id);
|
|
||||||
result.push({ ...ev, _calName: calName });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMeetingCard(m) {
|
|
||||||
const startStr = (m.start && (m.start.dateTime || m.start.date)) || '';
|
|
||||||
const endStr = (m.end && (m.end.dateTime || m.end.date)) || '';
|
|
||||||
const soon = startStr && endStr ? isSoonOrOngoing(startStr, endStr) : false;
|
|
||||||
const today = startStr ? isToday(startStr) : false;
|
|
||||||
|
|
||||||
const badges = (m._cals || []).map(c => {
|
|
||||||
if (c === 'ODOO4projects') return `<span class="badge badge-odoo">ODOO4</span>`;
|
|
||||||
if (c === 'Brandize') return `<span class="badge badge-brandize">Brandize</span>`;
|
|
||||||
return '';
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const dateLabel = (!today && startStr)
|
|
||||||
? `<span style="font-size:10px;opacity:0.55;margin-right:4px;">${formatLocalDate(startStr)}</span>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const timeLabel = startStr && endStr ? `${formatLocalTime(startStr)} – ${formatLocalTime(endStr)}` : '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="meeting${soon ? ' soon' : ''}">
|
|
||||||
<div class="meeting-header">
|
|
||||||
<div class="meeting-name">${m.summary || 'Meeting'}</div>
|
|
||||||
<div class="meeting-badges">${badges}</div>
|
|
||||||
</div>
|
|
||||||
<div class="time">${dateLabel}${timeLabel}</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Todos
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function buildTodoItem(t) {
|
|
||||||
const done = !!t.done;
|
|
||||||
const check = done
|
|
||||||
? `<svg width="10" height="10" viewBox="0 0 10 10"><polyline points="1.5,5 4,7.5 8.5,2.5" stroke="#fff" stroke-width="1.8" fill="none" stroke-linecap="round"/></svg>`
|
|
||||||
: '';
|
|
||||||
return `
|
|
||||||
<div class="todo${done ? ' done-todo' : ''}">
|
|
||||||
<div class="todo-check${done ? ' checked' : ''}">${check}</div>
|
|
||||||
<span>${t.Todotodo || ''}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Signup items — accepts a single object OR an array
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function buildSignupItem(ct) {
|
|
||||||
if (!ct) return '';
|
|
||||||
const tagBadges = (ct.tags || '').split(',').map(t => t.trim()).filter(Boolean)
|
|
||||||
.map(t => `<span class="signup-tag">${t}</span>`).join('');
|
|
||||||
return `
|
|
||||||
<div class="signup-item">
|
|
||||||
<div class="signup-email">${ct.email || ''}</div>
|
|
||||||
<div class="signup-meta">
|
|
||||||
<span class="signup-server">🖥 ${ct.server || ''}</span>
|
|
||||||
${tagBadges}
|
|
||||||
<span style="opacity:0.5;font-size:10px;">${ct.status || ''}</span>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:4px;opacity:0.55;font-size:10px;">
|
|
||||||
${ct.Domains ? ct.Domains : ''}${ct.expires ? ' · expires ' + ct.expires : ''}
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function render(data) {
|
|
||||||
// --- Servers ---
|
|
||||||
const serverStatusRaw = data['Server Status'] || [];
|
|
||||||
const serverMap = {};
|
|
||||||
for (const s of serverStatusRaw) { if (!serverMap[s.Server]) serverMap[s.Server] = s; }
|
|
||||||
const servers = Object.values(serverMap);
|
|
||||||
const serverCards = servers.map(buildServerCard);
|
|
||||||
const overallColor = worstColor(serverCards.map(c => c.light));
|
|
||||||
const serverCardsHtml = serverCards.map(c => c.html).join('');
|
|
||||||
|
|
||||||
// --- Meetings ---
|
|
||||||
const calOdoo = data['Calender ODOO4projects'] || [];
|
|
||||||
const calBrandize = data['Calender Brandize'] || [];
|
|
||||||
|
|
||||||
const allMeetings = [
|
|
||||||
...parseMeetings(calOdoo, 'ODOO4projects'),
|
|
||||||
...parseMeetings(calBrandize, 'Brandize')
|
|
||||||
];
|
|
||||||
|
|
||||||
const meetingById = {};
|
|
||||||
for (const m of allMeetings) {
|
|
||||||
if (!meetingById[m.id]) {
|
|
||||||
meetingById[m.id] = { ...m, _cals: [m._calName] };
|
|
||||||
} else if (!meetingById[m.id]._cals.includes(m._calName)) {
|
|
||||||
meetingById[m.id]._cals.push(m._calName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dedupedMeetings = Object.values(meetingById).sort((a, b) => {
|
|
||||||
const aS = (a.start && (a.start.dateTime || a.start.date)) || '';
|
|
||||||
const bS = (b.start && (b.start.dateTime || b.start.date)) || '';
|
|
||||||
return new Date(aS) - new Date(bS);
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Todos ---
|
|
||||||
const todos = data['Todos'] || [];
|
|
||||||
|
|
||||||
// --- Latest Signups: may be array or single object ---
|
|
||||||
const signupsRaw = data['Latest Signups'] || data['Signups'] || data['signups'] || [];
|
|
||||||
const signupsArr = Array.isArray(signupsRaw)
|
|
||||||
? signupsRaw
|
|
||||||
: (signupsRaw && typeof signupsRaw === 'object' ? [signupsRaw] : []);
|
|
||||||
const signupsHtml = signupsArr.length
|
|
||||||
? signupsArr.map(buildSignupItem).join('')
|
|
||||||
: '<div style="opacity:0.4;font-size:13px;">No signups</div>';
|
|
||||||
|
|
||||||
// --- Current Test: object ---
|
|
||||||
const currentTest = data['Current Tests'] || null;
|
|
||||||
|
|
||||||
document.getElementById('dashboard').innerHTML = `
|
|
||||||
|
|
||||||
<!-- SERVER STATUS -->
|
|
||||||
<div class="box" style="grid-column:1; grid-row:1/3;">
|
|
||||||
<div class="title">Server Status</div>
|
|
||||||
<div class="status-light ${overallColor}"></div>
|
|
||||||
<div class="servers">${serverCardsHtml}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- MEETINGS -->
|
|
||||||
<div class="box" style="grid-column:2; grid-row:1;">
|
|
||||||
<div class="title">Meetings</div>
|
|
||||||
<div class="meetings-scroll">
|
|
||||||
${dedupedMeetings.length
|
|
||||||
? dedupedMeetings.map(buildMeetingCard).join('')
|
|
||||||
: '<div style="opacity:0.4;font-size:13px;">No meetings</div>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- LATEST SIGNUPS -->
|
|
||||||
<div class="box" style="grid-column:3; grid-row:1;">
|
|
||||||
<div class="title">Latest Signups</div>
|
|
||||||
<div class="signups-scroll">${signupsHtml}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TODOS -->
|
|
||||||
<div class="box" style="grid-column:2; grid-row:2;">
|
|
||||||
<div class="title">Today's Todos</div>
|
|
||||||
<div class="todos-scroll">
|
|
||||||
${todos.length
|
|
||||||
? todos.map(buildTodoItem).join('')
|
|
||||||
: '<div style="opacity:0.4;font-size:13px;">No todos</div>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CURRENT TEST -->
|
|
||||||
<div class="box" style="grid-column:3; grid-row:2;">
|
|
||||||
<div class="title">Current Test</div>
|
|
||||||
${currentTest ? `
|
|
||||||
<div class="test-box">
|
|
||||||
<div class="test-row"><span class="test-label">UUID</span><span class="test-value" style="font-size:10px;">${currentTest.UUID || ''}</span></div>
|
|
||||||
<div class="test-row"><span class="test-label">Email</span><span class="test-value">${currentTest.email || ''}</span></div>
|
|
||||||
<div class="test-row"><span class="test-label">Server</span><span class="test-value">${currentTest.server || ''}</span></div>
|
|
||||||
<div class="test-row"><span class="test-label">Tags</span><span class="test-value"><span class="signup-tag">${currentTest.tags || ''}</span></span></div>
|
|
||||||
<div class="test-row"><span class="test-label">Status</span><span class="test-value"><span class="status-badge dev">${currentTest.status || ''}</span></span></div>
|
|
||||||
<div class="test-row"><span class="test-label">Expires</span><span class="test-value">${currentTest.expires || ''}</span></div>
|
|
||||||
<div class="test-row"><span class="test-label">Domains</span><span class="test-value" style="font-size:10px;">${currentTest.Domains || ''}</span></div>
|
|
||||||
<div class="test-row"><span class="test-label">Created</span><span class="test-value">${currentTest.created || ''}</span></div>
|
|
||||||
</div>` : '<div style="opacity:0.4;font-size:13px;">No active test</div>'}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Signal to Puppeteer that rendering is complete
|
|
||||||
window.__dashboardReady = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Fetch once, then signal ready
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
fetch(WEBHOOK_URL)
|
|
||||||
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
|
||||||
.then(data => render(data))
|
|
||||||
.catch(err => {
|
|
||||||
document.getElementById('dashboard').innerHTML =
|
|
||||||
`<div style="grid-column:1/-1;grid-row:1/-1;padding:40px;color:#e05d5d;font-size:14px;">
|
|
||||||
⚠ Failed to load: ${err.message}
|
|
||||||
</div>`;
|
|
||||||
window.__dashboardReady = true;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
Here's what changed and why:
|
|
||||||
|
|
||||||
**Puppeteer fix — `window.__dashboardReady` flag**
|
|
||||||
Instead of relying on `networkidle` (which races against the async fetch), Puppeteer should now wait for this flag:
|
|
||||||
```/dev/null/puppeteer-example.js#L1-4
|
|
||||||
await page.goto('file:///.../dasshboard.html');
|
|
||||||
await page.waitForFunction(() => window.__dashboardReady === true);
|
|
||||||
await page.screenshot({ path: 'dashboard.png' });
|
|
||||||
```
|
|
||||||
The flag is set to `true` at the end of `render()` (and also in the catch block), so Puppeteer only screenshots once the DOM is fully painted — never the loading screen.
|
|
||||||
|
|
||||||
**Latest Signups fix**
|
|
||||||
The code now tries three key names — `Latest Signups`, `Signups`, `signups` — and handles both an **array** and a **single object** (wraps it in an array). Once your webhook returns the signups as an array under one of those keys, all items will render. Right now the webhook has no array of signups, only `Current Tests` as a single object, so the box will show "No signups" until the correct key is available in the webhook response.
|
|
||||||
Reference in New Issue
Block a user