diff --git a/config-example.yml b/config-example.yml
index 0d193e2..932aecf 100644
--- a/config-example.yml
+++ b/config-example.yml
@@ -30,20 +30,33 @@ services:
timeout_seconds: 5
info:
- label: "Commits totaux"
- source: "rss_multi"
- urls:
+ source: "gitea_multi"
+ base_url: ""
+ owner: ""
+ repos:
- ""
- path: "rss.channel.item"
aggregate: "count"
+ token_env: "GITEA_TOKEN"
+ fallback_urls:
+ - ""
- label: "Dernier commit"
- source: "rss_multi"
- urls:
+ source: "gitea_multi"
+ base_url: ""
+ owner: ""
+ repos:
- ""
- path: "rss.channel.item"
aggregate: "latest_title"
- - label: "Publié le"
- source: "rss_multi"
- urls:
+ token_env: "GITEA_TOKEN"
+ fallback_urls:
+ - ""
+ - label: "Publié le"
+ source: "gitea_multi"
+ base_url: ""
+ owner: ""
+ repos:
- ""
- path: "rss.channel.item"
aggregate: "latest_pubdate"
+ token_env: "GITEA_TOKEN"
+ transform: "discord_time"
+ fallback_urls:
+ - ""
diff --git a/src/bot.js b/src/bot.js
index c9823b9..2959c7b 100644
--- a/src/bot.js
+++ b/src/bot.js
@@ -1,14 +1,25 @@
-require('dotenv').config();
+const path = require('path');
+const dotenv = require('dotenv');
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');
const { fetch } = require('undici');
const fs = require('fs');
const yaml = require('js-yaml');
+const envPath = path.resolve(__dirname, '..', '.env');
+const envResult = dotenv.config({ path: envPath });
+if (envResult.error) {
+ // Fallback to default resolution when the project root already matches process.cwd().
+ dotenv.config();
+}
+
const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
const STATUS_GUILD_ID = process.env.STATUS_GUILD_ID;
const STATUS_CHANNEL_ID = process.env.STATUS_CHANNEL_ID;
const ADMIN_USER_ID = process.env.ADMIN_USER_ID;
const CONFIG_PATH = process.env.CONFIG_PATH || './config.yml';
+const RESOLVED_CONFIG_PATH = path.isAbsolute(CONFIG_PATH)
+ ? CONFIG_PATH
+ : path.resolve(__dirname, '..', CONFIG_PATH);
const REFRESH_INTERVAL = parseInt(process.env.REFRESH_INTERVAL || '60', 10);
const STICKY_MESSAGE = String(process.env.STICKY_MESSAGE || 'true').toLowerCase() === 'true';
@@ -27,7 +38,7 @@ for (const [name, value] of Object.entries(requiredEnv)) {
}
function loadConfig() {
- const text = fs.readFileSync(CONFIG_PATH, 'utf8');
+ const text = fs.readFileSync(RESOLVED_CONFIG_PATH, 'utf8');
return yaml.load(text);
}
@@ -99,29 +110,59 @@ function extractTag(text, tag) {
return match ? decodeXmlEntities(match[1].trim()) : null;
}
+function extractAtomLink(text) {
+ if (!text || typeof text !== 'string') return null;
+ const match = text.match(/]*href=['"]([^'"]+)['"][^>]*>/i);
+ if (match && match[1]) return decodeXmlEntities(match[1].trim());
+ return extractTag(text, 'link');
+}
+
function parseRss(text) {
- const channelMatch = /([\s\S]*?)<\/channel>/i.exec(text);
- const channelText = channelMatch ? channelMatch[1] : text;
+ const channelMatch = /]*>([\s\S]*?)<\/channel>/i.exec(text);
+ const feedMatch = /]*>([\s\S]*?)<\/feed>/i.exec(text);
+ const channelText = channelMatch ? channelMatch[1] : (feedMatch ? feedMatch[1] : text);
const items = [];
- const itemRegex = /- ([\s\S]*?)<\/item>/gi;
+ const itemRegex = /
- ]*>([\s\S]*?)<\/item>/gi;
let itemMatch;
while ((itemMatch = itemRegex.exec(channelText))) {
const item = itemMatch[1];
items.push({
title: extractTag(item, 'title'),
link: extractTag(item, 'link'),
- description: extractTag(item, 'description'),
+ description: extractTag(item, 'description') || extractTag(item, 'content:encoded'),
author: extractTag(item, 'author'),
- pubDate: extractTag(item, 'pubDate'),
- guid: extractTag(item, 'guid'),
+ pubDate: extractTag(item, 'pubDate') || extractTag(item, 'published') || extractTag(item, 'updated'),
+ guid: extractTag(item, 'guid') || extractTag(item, 'id'),
+ updated: extractTag(item, 'updated'),
+ published: extractTag(item, 'published'),
});
}
+
+ if (!items.length && feedMatch) {
+ const entryRegex = /]*>([\s\S]*?)<\/entry>/gi;
+ let entryMatch;
+ while ((entryMatch = entryRegex.exec(feedMatch[1]))) {
+ const entry = entryMatch[1];
+ const fullEntry = entryMatch[0];
+ items.push({
+ title: extractTag(entry, 'title'),
+ link: extractAtomLink(fullEntry),
+ description: extractTag(entry, 'summary') || extractTag(entry, 'content'),
+ author: extractTag(entry, 'author'),
+ pubDate: extractTag(entry, 'published') || extractTag(entry, 'updated'),
+ guid: extractTag(entry, 'id'),
+ updated: extractTag(entry, 'updated'),
+ published: extractTag(entry, 'published'),
+ });
+ }
+ }
+
return {
rss: {
channel: {
title: extractTag(channelText, 'title'),
- link: extractTag(channelText, 'link'),
- description: extractTag(channelText, 'description'),
+ link: extractAtomLink(channelMatch ? channelMatch[0] : (feedMatch ? feedMatch[0] : channelText)),
+ description: extractTag(channelText, 'description') || extractTag(channelText, 'subtitle'),
item: items,
},
},
@@ -137,6 +178,12 @@ function normalizeRssItem(item) {
const out = { ...item };
if (out.title) out.title = stripHtml(out.title);
if (out.pubDate) out.pubDate = stripHtml(out.pubDate);
+ if (!out.pubDate) {
+ if (out.published) out.pubDate = stripHtml(out.published);
+ else if (out.updated) out.pubDate = stripHtml(out.updated);
+ }
+ if (out.published) out.published = stripHtml(out.published);
+ if (out.updated) out.updated = stripHtml(out.updated);
return out;
}
return null;
@@ -148,7 +195,11 @@ async function fetchRssWithTimeout(url, timeoutMs) {
try {
const resp = await fetch(url, { signal: ctrl.signal });
clearTimeout(timer);
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ if (!resp.ok) {
+ const err = new Error(`HTTP ${resp.status}`);
+ err.status = resp.status;
+ throw err;
+ }
const text = await resp.text();
return parseRss(text);
} finally {
@@ -156,6 +207,250 @@ async function fetchRssWithTimeout(url, timeoutMs) {
}
}
+async function gatherRssItems(urls, path, dataCache, timeoutMs) {
+ const items = [];
+ for (const url of urls) {
+ if (!url) continue;
+ let cached = dataCache.get(`rss:${url}`);
+ if (!cached) {
+ cached = await fetchRssWithTimeout(url, timeoutMs);
+ dataCache.set(`rss:${url}`, cached);
+ }
+ const rssPath = path || 'rss.channel.item';
+ const pathVal = getValueAtPath(cached, rssPath);
+ if (Array.isArray(pathVal)) {
+ for (const entry of pathVal) {
+ const norm = normalizeRssItem(entry);
+ if (norm) items.push(norm);
+ }
+ } else {
+ const norm = normalizeRssItem(pathVal);
+ if (norm) items.push(norm);
+ }
+ }
+ return items;
+}
+
+function aggregateRssItems(items, aggregate) {
+ if (aggregate === 'count') {
+ return items.length;
+ }
+
+ if (aggregate === 'latest_title' || aggregate === 'latest_pubdate') {
+ let latest = null;
+ for (const entry of items) {
+ const date = entry && entry.pubDate ? new Date(entry.pubDate) : null;
+ const timestamp = date && !Number.isNaN(date.getTime()) ? date.getTime() : null;
+ if (!latest || (timestamp !== null && timestamp > latest.timestamp)) {
+ latest = { entry, timestamp };
+ }
+ }
+ if (!latest) return undefined;
+ if (aggregate === 'latest_title') {
+ return latest.entry.title || undefined;
+ }
+ if (latest.timestamp) {
+ return new Date(latest.timestamp).toISOString();
+ }
+ return latest.entry.pubDate || undefined;
+ }
+
+ return items.map(entry => entry.title).filter(Boolean).join(', ');
+}
+
+function extractFirstCommitMessageFromDescription(description) {
+ if (!description || typeof description !== 'string') return null;
+ const liMatch = description.match(/]*>([\s\S]*?)<\/li>/i);
+ if (!liMatch) return null;
+ const liContent = liMatch[1];
+ const anchorMatch = liContent.match(/]*>([\s\S]*?)<\/a>/i);
+ const text = anchorMatch ? anchorMatch[1] : liContent;
+ const cleaned = stripHtml(text).replace(/\s+/g, ' ').trim();
+ return cleaned || null;
+}
+
+function buildCommitInfo(prefix, branch, commit) {
+ if (!commit) return null;
+ const message = (commit.summary || commit.message || '').trim();
+ const title = message || prefix;
+ let location = prefix || '';
+ if (branch) {
+ location = location ? `${location} (${branch})` : branch;
+ }
+ const detailParts = [];
+ if (location) detailParts.push(location);
+ if (commit.author) detailParts.push(`par ${commit.author}`);
+ const details = detailParts.join(' — ');
+ return { title, details };
+}
+
+function computeGiteaFallbackAggregate(items, aggregate) {
+ if (aggregate === 'count') return items.length;
+
+ let latest = null;
+ for (const entry of items) {
+ if (!entry) continue;
+ const date = entry.pubDate ? new Date(entry.pubDate) : null;
+ const timestamp = date && !Number.isNaN(date.getTime()) ? date.getTime() : null;
+ if (!latest || (timestamp !== null && timestamp > latest.timestamp)) {
+ latest = { entry, timestamp };
+ }
+ }
+
+ if (!latest) return undefined;
+
+ if (aggregate === 'latest_pubdate') {
+ if (latest.timestamp) {
+ return new Date(latest.timestamp).toISOString();
+ }
+ return latest.entry.pubDate || undefined;
+ }
+
+ if (aggregate === 'latest_title') {
+ const title = stripHtml(latest.entry.title || '');
+ const message = extractFirstCommitMessageFromDescription(latest.entry.description) || '';
+ if (title || message) {
+ const primary = message || title;
+ const secondary = title && title !== primary ? title : (message && message !== primary ? message : '');
+ const info = { title: primary };
+ if (secondary) info.details = secondary;
+ return info;
+ }
+ return undefined;
+ }
+
+ return items.map(entry => entry.title).filter(Boolean).join(', ');
+}
+
+function normalizeGiteaRepos(repos, defaultOwner) {
+ if (!repos) return [];
+ const list = Array.isArray(repos) ? repos : [repos];
+ const out = [];
+ for (const entry of list) {
+ if (!entry) continue;
+ if (typeof entry === 'string') {
+ const trimmed = entry.trim();
+ if (!trimmed) continue;
+ if (trimmed.includes('/')) {
+ const [owner, repo] = trimmed.split('/', 2);
+ if (owner && repo) {
+ out.push({
+ owner,
+ repo,
+ branch: null,
+ displayName: `${owner}/${repo}`,
+ });
+ }
+ } else if (defaultOwner) {
+ out.push({
+ owner: defaultOwner,
+ repo: trimmed,
+ branch: null,
+ displayName: `${defaultOwner}/${trimmed}`,
+ });
+ }
+ } else if (typeof entry === 'object') {
+ const owner = entry.owner || defaultOwner;
+ const repo = entry.repo;
+ if (!owner || !repo) continue;
+ out.push({
+ owner,
+ repo,
+ branch: entry.branch || null,
+ displayName: entry.display_name || entry.displayName || `${owner}/${repo}`,
+ });
+ }
+ }
+ return out;
+}
+
+function resolveGiteaToken(info) {
+ if (info && typeof info === 'object') {
+ if (info.token) return info.token;
+ if (info.token_env && process.env[info.token_env]) return process.env[info.token_env];
+ }
+ return process.env.GITEA_TOKEN || process.env.GITEA_ACCESS_TOKEN || null;
+}
+
+function shouldUseGiteaFallback(error) {
+ const status = error && typeof error === 'object' ? error.status : null;
+ return status === 401 || status === 403 || status === 404;
+}
+
+function pickGiteaCommitTimestamp(commit) {
+ const candidates = [
+ commit?.commit?.author?.date,
+ commit?.commit?.committer?.date,
+ commit?.author?.date,
+ commit?.committer?.date,
+ ];
+ for (const candidate of candidates) {
+ if (!candidate) continue;
+ const ts = Date.parse(candidate);
+ if (!Number.isNaN(ts)) return ts;
+ }
+ return null;
+}
+
+async function fetchGiteaRepoSummary({ baseUrl, owner, repo, branch, token, timeoutMs }) {
+ const ctrl = new AbortController();
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
+ try {
+ const url = new URL(`/api/v1/repos/${owner}/${repo}/commits`, baseUrl);
+ url.searchParams.set('limit', '1');
+ if (branch) url.searchParams.set('sha', branch);
+
+ const headers = {};
+ if (token) headers.Authorization = `token ${token}`;
+
+ const resp = await fetch(url, { signal: ctrl.signal, headers });
+ clearTimeout(timer);
+ if (!resp.ok) {
+ const err = new Error(`HTTP ${resp.status}`);
+ err.status = resp.status;
+ throw err;
+ }
+
+ const text = await resp.text();
+ let commits = [];
+ if (text.trim()) {
+ try {
+ commits = JSON.parse(text);
+ } catch {
+ commits = [];
+ }
+ }
+
+ const totalHeader = resp.headers.get('x-total-count');
+ const totalCommits = totalHeader && /^\d+$/.test(totalHeader) ? Number(totalHeader) : null;
+
+ const latestCommitRaw = Array.isArray(commits) && commits.length ? commits[0] : null;
+ const latestCommit = latestCommitRaw
+ ? {
+ sha: latestCommitRaw.sha || latestCommitRaw.id || null,
+ message: latestCommitRaw.commit?.message || latestCommitRaw.message || '',
+ summary: (() => {
+ const msg = latestCommitRaw.commit?.message || latestCommitRaw.message || '';
+ return msg ? msg.split(/\r?\n/)[0].trim() : '';
+ })(),
+ author:
+ latestCommitRaw.commit?.author?.name ||
+ latestCommitRaw.author?.login ||
+ latestCommitRaw.author?.username ||
+ latestCommitRaw.author?.name ||
+ latestCommitRaw.commit?.committer?.name ||
+ null,
+ url: latestCommitRaw.html_url || latestCommitRaw.url || latestCommitRaw.commit?.url || null,
+ timestamp: pickGiteaCommitTimestamp(latestCommitRaw),
+ }
+ : null;
+
+ return { totalCommits, latestCommit };
+ } finally {
+ clearTimeout(timer);
+ }
+}
+
function applyInfoTransform(info, value) {
if (value === undefined || value === null) return value;
if (!info || typeof info !== 'object') return value;
@@ -232,51 +527,93 @@ async function collectExtras(svc, context) {
case 'rss_multi': {
const urls = Array.isArray(info.urls) && info.urls.length ? info.urls : (info.url ? [info.url] : []);
if (!urls.length) throw new Error('URLs manquantes pour rss_multi');
- const items = [];
- for (const url of urls) {
- let cached = dataCache.get(`rss:${url}`);
+ const items = await gatherRssItems(urls, info.path, dataCache, context.timeoutMs);
+ value = aggregateRssItems(items, info.aggregate);
+ break;
+ }
+ case 'gitea_multi': {
+ const baseUrlRaw = info.base_url || svc.health_url || '';
+ if (!baseUrlRaw) throw new Error('URL manquante pour gitea_multi');
+ const baseUrl = baseUrlRaw.replace(/\/+$/, '');
+ const repoEntries = normalizeGiteaRepos(info.repos, info.owner);
+ if (!repoEntries.length) throw new Error('Dépôts manquants pour gitea_multi');
+
+ const authToken = resolveGiteaToken(info);
+ const summaries = [];
+ let fetchError = null;
+ for (const repoEntry of repoEntries) {
+ const cacheKey = `gitea:${baseUrl}:${repoEntry.owner}/${repoEntry.repo}:${repoEntry.branch || ''}`;
+ let cached = dataCache.get(cacheKey);
if (!cached) {
- cached = await fetchRssWithTimeout(url, context.timeoutMs);
- dataCache.set(`rss:${url}`, cached);
- }
- const rssPath = info.path || 'rss.channel.item';
- const pathVal = getValueAtPath(cached, rssPath);
- if (Array.isArray(pathVal)) {
- for (const entry of pathVal) {
- const norm = normalizeRssItem(entry);
- if (norm) items.push(norm);
+ try {
+ cached = await fetchGiteaRepoSummary({
+ baseUrl,
+ owner: repoEntry.owner,
+ repo: repoEntry.repo,
+ branch: repoEntry.branch,
+ token: authToken,
+ timeoutMs: context.timeoutMs,
+ });
+ dataCache.set(cacheKey, cached);
+ } catch (err) {
+ fetchError = err;
+ break;
}
- } else {
- const norm = normalizeRssItem(pathVal);
- if (norm) items.push(norm);
}
+ summaries.push({ repo: repoEntry, summary: cached });
+ }
+
+ if (fetchError) {
+ const fallbackUrls = Array.isArray(info.fallback_urls) ? info.fallback_urls.filter(Boolean) : [];
+ if (fallbackUrls.length && shouldUseGiteaFallback(fetchError)) {
+ const items = await gatherRssItems(fallbackUrls, info.fallback_path || info.path, dataCache, context.timeoutMs);
+ value = computeGiteaFallbackAggregate(items, info.aggregate);
+ break;
+ }
+ throw fetchError;
}
if (info.aggregate === 'count') {
- value = items.length;
+ let total = 0;
+ for (const { summary } of summaries) {
+ if (summary.totalCommits !== null && summary.totalCommits !== undefined) {
+ total += summary.totalCommits;
+ } else {
+ total = null;
+ break;
+ }
+ }
+ value = total !== null ? total : undefined;
} else if (info.aggregate === 'latest_title' || info.aggregate === 'latest_pubdate') {
let latest = null;
- for (const entry of items) {
- const date = entry.pubDate ? new Date(entry.pubDate) : null;
- const timestamp = date && !Number.isNaN(date.getTime()) ? date.getTime() : null;
- if (!latest || (timestamp !== null && timestamp > latest.timestamp)) {
- latest = { entry, timestamp };
+ for (const { repo, summary } of summaries) {
+ const commit = summary.latestCommit;
+ if (!commit) continue;
+ const timestamp = commit.timestamp;
+ if (timestamp === null || timestamp === undefined) continue;
+ if (!latest || timestamp > latest.timestamp) {
+ latest = { repo, commit, timestamp };
}
}
if (!latest) {
value = undefined;
} else if (info.aggregate === 'latest_title') {
- value = latest.entry.title || undefined;
+ const prefix = latest.repo.displayName || `${latest.repo.owner}/${latest.repo.repo}`;
+ value = buildCommitInfo(prefix, latest.repo.branch || null, latest.commit);
} else {
- if (latest.timestamp) {
- const unixSeconds = Math.floor(latest.timestamp / 1000);
- value = ``;
- } else {
- value = undefined;
- }
+ value = new Date(latest.timestamp).toISOString();
}
} else {
- value = items.map(entry => entry.title).filter(Boolean).join(', ');
+ value = summaries
+ .map(({ repo, summary }) => {
+ const message = summary.latestCommit?.message;
+ if (!message) return null;
+ const firstLine = stripHtml(message.split(/\r?\n/)[0]);
+ const prefix = repo.displayName || `${repo.owner}/${repo.repo}`;
+ return `${prefix}: ${firstLine}`;
+ })
+ .filter(Boolean)
+ .join(', ');
}
break;
}
@@ -314,8 +651,15 @@ const metricNameOverrides = {
ping_ms: 'Ping',
server_ping_ms: 'Ping',
mare_ping_ms: 'Ping',
+ mare_authorized_connections_peak: 'Pic de connexions',
+ mare_authorized_connections_highwater: 'Pic de connexions',
+ mare_authorized_connections_max: 'Pic de connexions',
+ mare_authorized_connections_record: 'Pic de connexions',
+ _peak_authorized_connections: 'Pic de connexions',
};
+const connectionPeaks = new Map();
+
function pickKeyMetrics(name, all) {
const perService = {
'Serveur de synchronisation': [
@@ -350,7 +694,41 @@ function choosePingMetric(allMetrics) {
return null;
}
-function formatMetrics(name, allMetrics, metricsByService) {
+function asFiniteNumber(value) {
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
+ if (typeof value === 'string') {
+ const trimmed = value.trim();
+ if (!trimmed) return null;
+ const num = Number(trimmed);
+ if (Number.isFinite(num)) return num;
+ }
+ return null;
+}
+
+function pickPeakMetric(primary, fallback) {
+ const candidates = [
+ 'mare_authorized_connections_peak',
+ 'mare_authorized_connections_highwater',
+ 'mare_authorized_connections_max',
+ 'mare_authorized_connections_record',
+ ];
+
+ for (const key of candidates) {
+ if (primary && Object.prototype.hasOwnProperty.call(primary, key)) {
+ const val = asFiniteNumber(primary[key]);
+ if (val !== null) return { key, value: val };
+ }
+ }
+ for (const key of candidates) {
+ if (fallback && Object.prototype.hasOwnProperty.call(fallback, key)) {
+ const val = asFiniteNumber(fallback[key]);
+ if (val !== null) return { key, value: val };
+ }
+ }
+ return null;
+}
+
+function formatMetrics(name, allMetrics, metricsByService, peakStore = null) {
const selected = pickKeyMetrics(name, allMetrics);
const entries = [];
const skipKeys = new Set(['ping_ms', 'server_ping_ms', 'mare_ping_ms']);
@@ -359,16 +737,37 @@ function formatMetrics(name, allMetrics, metricsByService) {
const fallbackMetrics = fallbackName ? (metricsByService[fallbackName] || {}) : {};
if (name === "Serveur d'identification") {
- const usersConnected = allMetrics.mare_authorized_connections ?? fallbackMetrics.mare_authorized_connections;
- const usersRegistered = allMetrics.mare_users_registered ?? fallbackMetrics.mare_users_registered;
+ const usersConnectedRaw = allMetrics.mare_authorized_connections ?? fallbackMetrics.mare_authorized_connections;
+ const usersRegisteredRaw = allMetrics.mare_users_registered ?? fallbackMetrics.mare_users_registered;
+
+ entries.push([
+ 'mare_users_registered',
+ usersRegisteredRaw !== undefined ? usersRegisteredRaw : 'N/A',
+ ]);
+
+ let peakEntry = pickPeakMetric(allMetrics, fallbackMetrics);
+
+ if (peakStore) {
+ if (peakEntry) {
+ peakStore.set(name, peakEntry.value);
+ } else {
+ const numericConnected = asFiniteNumber(usersConnectedRaw);
+ if (numericConnected !== null) {
+ const prev = peakStore.get(name);
+ const nextPeak = prev !== undefined ? Math.max(prev, numericConnected) : numericConnected;
+ peakStore.set(name, nextPeak);
+ peakEntry = { key: '_peak_authorized_connections', value: nextPeak };
+ }
+ }
+ }
+
+ if (peakEntry) {
+ entries.push([peakEntry.key, peakEntry.value]);
+ }
entries.push([
'mare_authorized_connections',
- usersConnected !== undefined ? usersConnected : 'N/A',
- ]);
- entries.push([
- 'mare_users_registered',
- usersRegistered !== undefined ? usersRegistered : 'N/A',
+ usersConnectedRaw !== undefined ? usersConnectedRaw : 'N/A',
]);
} else {
for (const key of Object.keys(selected)) {
@@ -427,7 +826,22 @@ function formatExtraValue(value) {
return value.map(v => formatExtraValue(v)).join(', ');
}
if (value && typeof value === 'object') {
- return Object.values(value).map(v => formatExtraValue(v)).join(', ');
+ const hasTitle = Object.prototype.hasOwnProperty.call(value, 'title');
+ const hasDetails = Object.prototype.hasOwnProperty.call(value, 'details');
+ if (hasTitle || hasDetails) {
+ const parts = [];
+ if (hasTitle && value.title !== undefined && value.title !== null && value.title !== '') {
+ parts.push(formatExtraValue(value.title));
+ }
+ if (hasDetails && value.details !== undefined && value.details !== null && value.details !== '') {
+ parts.push(formatExtraValue(value.details));
+ }
+ return parts.join(' — ');
+ }
+ return Object.values(value)
+ .filter(v => v !== undefined && v !== null && v !== '')
+ .map(v => formatExtraValue(v))
+ .join(', ');
}
if (typeof value === 'string') {
value = stripHtml(value);
@@ -576,7 +990,7 @@ function buildEmbed(results) {
const m = { ...(status.metrics || {}) };
if (typeof status.ping_ms === 'number') m.ping_ms = status.ping_ms;
- const entries = formatMetrics(name, m, metricsByService).slice(0, 6);
+ const entries = formatMetrics(name, m, metricsByService, connectionPeaks).slice(0, 6);
if (entries.length) {
const pretty = entries.map(([k, v]) => `- ${prettyMetricName(k)}: ${prettyMetricValue(k, v)}`).join('\n');
value += `\n${pretty}`;