diff --git a/server/config.example.yaml b/server/config.example.yaml
index 2b8fc520..1d2f0106 100644
--- a/server/config.example.yaml
+++ b/server/config.example.yaml
@@ -7,6 +7,7 @@ admins:
password_hash: "$2b$10$n0L0oe1ZQy7IgM.FvVAzb.aXz43uaZWFiT0wr.05uNoVIDLawmrCG" # password: lockdownpass
discord_id: "0987654321"
lockdown: true
+timezone: "America/New_York"
media:
# Base address for mediaMTX (scheme + host + optional port/path). The UI will always request
# http:////whep
diff --git a/server/src/services/discordBotService.js b/server/src/services/discordBotService.js
index 03de1c3b..c384e01d 100644
--- a/server/src/services/discordBotService.js
+++ b/server/src/services/discordBotService.js
@@ -192,6 +192,7 @@ function formatHelp() {
'`rs lock ` — lock a rover',
'`rs unlock ` — unlock a rover',
'`rs mode ` — change server mode',
+ '`ts` — show time status',
].join('\n');
}
@@ -394,7 +395,12 @@ async function handleModeCommand(message, mode) {
async function handleCommand(message) {
if (message.author.bot) return;
const content = (message.content || '').trim();
- if (!content.toLowerCase().startsWith('rs')) return;
+ const lower = content.toLowerCase();
+ if (lower === 'ts' || lower.startsWith('ts ')) {
+ await handleTimeStatusCommand(message);
+ return;
+ }
+ if (!lower.startsWith('rs')) return;
const tokens = content.split(/\s+/);
tokens.shift(); // remove prefix
@@ -447,7 +453,8 @@ async function handleBridgeInbound(message) {
if (String(message.channelId) !== String(bridgeChannelId)) return;
if (message.author.bot) return;
const content = (message.content || '').trim();
- if (content.toLowerCase().startsWith('rs')) return; // don't echo commands
+ const lower = content.toLowerCase();
+ if (lower.startsWith('rs') || lower === 'ts' || lower.startsWith('ts ')) return; // don't echo commands
const nickname =
message.member?.nickname || message.author?.globalName || message.author?.username || 'Discord';
const role = isAdminUser(message.author.id) ? 'admin' : 'user';
@@ -470,6 +477,72 @@ function buildEmbed({ title, description, color }) {
return embed;
}
+function getServerTimezone() {
+ return config.timezone || config.server?.timezone || process.env.TZ || 'America/New_York';
+}
+
+function formatTimeInZone(date, timeZone) {
+ try {
+ const formatter = new Intl.DateTimeFormat('en-US', {
+ timeZone,
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ });
+ return formatter.format(date);
+ } catch (err) {
+ return 'n/a';
+ }
+}
+
+function buildTimeStatusEmbed() {
+ const serverTimezone = getServerTimezone();
+ const now = new Date();
+ const zones = [
+ { label: 'UTC', zone: 'UTC' },
+ { label: 'US Pacific', zone: 'America/Los_Angeles' },
+ { label: 'US Mountain', zone: 'America/Denver' },
+ { label: 'US Central', zone: 'America/Chicago' },
+ { label: 'US Eastern', zone: 'America/New_York' },
+ { label: 'Europe London', zone: 'Europe/London' },
+ { label: 'Europe Berlin', zone: 'Europe/Berlin' },
+ { label: 'Asia Kolkata', zone: 'Asia/Kolkata' },
+ { label: 'Asia Shanghai', zone: 'Asia/Shanghai' },
+ { label: 'Asia Tokyo', zone: 'Asia/Tokyo' },
+ { label: 'Australia Sydney', zone: 'Australia/Sydney' },
+ ];
+
+ const entries = zones.map((entry) => {
+ const time = formatTimeInZone(now, entry.zone);
+ const highlight =
+ String(entry.zone).toLowerCase() === String(serverTimezone).toLowerCase()
+ ? ' **(server local timezone)**'
+ : '';
+ return `${entry.label} — ${time}${highlight}`;
+ });
+
+ if (!zones.some((entry) => String(entry.zone).toLowerCase() === String(serverTimezone).toLowerCase())) {
+ const time = formatTimeInZone(now, serverTimezone);
+ entries.push(`Server Local — ${time} **(server local timezone)**`);
+ }
+
+ const embed = buildEmbed({
+ title: 'Time Status',
+ description: entries.join('\n'),
+ color: 0x2196f3,
+ });
+ embed.setFooter({ text: `Server local timezone: ${serverTimezone}` });
+ return embed;
+}
+
+async function handleTimeStatusCommand(message) {
+ const embed = buildTimeStatusEmbed();
+ await message.reply({
+ embeds: [embed],
+ allowedMentions: { parse: [], repliedUser: false },
+ });
+}
+
function buildBatteryStatusEmbed(color, records = null) {
const embed = buildEmbed({ title: 'Rover Battery Status', color: color || 0x2196f3 });
const baseRecords = records || Array.from(rovers.values());