mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
analytics and embed meta update!!!
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
// Site Metadata Helper
|
||||
// Purpose: Resolves the public name, description, and colors used before the web UI starts.
|
||||
// Scope: Keeps document/PWA branding server-rendered and independent of Socket.IO session state.
|
||||
const { loadConfig } = require('./configLoader');
|
||||
|
||||
const DEFAULT_SITE_METADATA = Object.freeze({
|
||||
name: 'Multi Roomba Rover',
|
||||
shortName: 'Multi Roomba Rover',
|
||||
description: 'Drive and watch remote rovers from your browser.',
|
||||
accentColor: '#38bdf8',
|
||||
backgroundColor: '#020617',
|
||||
publicUrl: null,
|
||||
});
|
||||
|
||||
const BACKGROUND_BLEND_AMOUNT = 0.15;
|
||||
|
||||
function asTrimmedString(value) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function normalizeHexColor(value) {
|
||||
const color = asTrimmedString(value).toLowerCase();
|
||||
|
||||
/*
|
||||
Supporting both common CSS hex forms keeps the operator-facing setting
|
||||
forgiving while still preventing arbitrary CSS from being injected into
|
||||
generated HTML and SVG attributes.
|
||||
*/
|
||||
if (/^#[0-9a-f]{6}$/.test(color)) return color;
|
||||
if (/^#[0-9a-f]{3}$/.test(color)) {
|
||||
return `#${color.slice(1).split('').map((character) => character.repeat(2)).join('')}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function blendHexColors(baseColor, accentColor, accentAmount) {
|
||||
const base = baseColor.slice(1).match(/.{2}/g).map((channel) => Number.parseInt(channel, 16));
|
||||
const accent = accentColor.slice(1).match(/.{2}/g).map((channel) => Number.parseInt(channel, 16));
|
||||
|
||||
/*
|
||||
The profile color is deliberately only a tint. A full-strength profile
|
||||
color could produce a glaring PWA launch screen, while this blend preserves
|
||||
the application's established dark appearance and still makes each server
|
||||
visually recognizable.
|
||||
*/
|
||||
const channels = base.map((channel, index) =>
|
||||
Math.round(channel * (1 - accentAmount) + accent[index] * accentAmount),
|
||||
);
|
||||
return `#${channels.map((channel) => channel.toString(16).padStart(2, '0')).join('')}`;
|
||||
}
|
||||
|
||||
function normalizePublicUrl(value) {
|
||||
const candidate = asTrimmedString(value);
|
||||
if (!candidate) return null;
|
||||
|
||||
/*
|
||||
URL() helpfully repairs strings such as `http:192.168.0.1`, but preserving
|
||||
that typo in public metadata would conceal a configuration mistake. Require
|
||||
the conventional absolute URL form so the published address is explicit.
|
||||
*/
|
||||
if (!/^https?:\/\//i.test(candidate)) return null;
|
||||
|
||||
try {
|
||||
const url = new URL(candidate);
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
||||
|
||||
/*
|
||||
Removing a trailing slash gives callers one stable base URL to combine
|
||||
with paths. Invalid values are ignored instead of producing broken
|
||||
canonical and social metadata on every page.
|
||||
*/
|
||||
return url.toString().replace(/\/$/, '');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getReadableAccentText(accentColor) {
|
||||
const channels = accentColor.slice(1).match(/.{2}/g).map((channel) => Number.parseInt(channel, 16));
|
||||
const luminance = (channels[0] * 299 + channels[1] * 587 + channels[2] * 114) / 1000;
|
||||
|
||||
// A simple luminance split keeps the generated preview badge legible for both dark and light profile colors.
|
||||
return luminance > 150 ? '#020617' : '#ffffff';
|
||||
}
|
||||
|
||||
function resolveSiteMetadata(config = loadConfig()) {
|
||||
const interInstance = config?.interInstance;
|
||||
const profile = interInstance?.profile;
|
||||
const profileName = asTrimmedString(profile?.name);
|
||||
|
||||
/*
|
||||
A partially filled profile must not unexpectedly rename the site. The
|
||||
inter-instance feature must be explicitly enabled and have a usable name
|
||||
before any profile branding is applied; otherwise every value comes from
|
||||
the coherent default set above.
|
||||
*/
|
||||
if (interInstance?.enabled !== true || !profileName) {
|
||||
return { ...DEFAULT_SITE_METADATA, accentTextColor: getReadableAccentText(DEFAULT_SITE_METADATA.accentColor) };
|
||||
}
|
||||
|
||||
const accentColor = normalizeHexColor(profile.color) || DEFAULT_SITE_METADATA.accentColor;
|
||||
return {
|
||||
name: profileName,
|
||||
shortName: profileName,
|
||||
description: asTrimmedString(profile.description) || DEFAULT_SITE_METADATA.description,
|
||||
accentColor,
|
||||
backgroundColor: blendHexColors(
|
||||
DEFAULT_SITE_METADATA.backgroundColor,
|
||||
accentColor,
|
||||
BACKGROUND_BLEND_AMOUNT,
|
||||
),
|
||||
accentTextColor: getReadableAccentText(accentColor),
|
||||
publicUrl: normalizePublicUrl(profile.publicUrl),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_SITE_METADATA,
|
||||
resolveSiteMetadata,
|
||||
};
|
||||
Reference in New Issue
Block a user