branding och namechange

This commit is contained in:
larssand
2026-08-28 20:56:58 +02:00
parent 5cab3a0e17
commit 3cf68cf34a
23 changed files with 146 additions and 89 deletions

View File

@@ -30,6 +30,8 @@ import { renderTeamStintLog as renderTeamStintLogHelper, renderSessionChannelAna
import { getManualCorrectionSummary as getManualCorrectionSummaryLogic, applyCompetitorCorrection as applyCompetitorCorrectionLogic, recalculateCompetitorFromPassings as recalculateCompetitorFromPassingsLogic, invalidateCompetitorLastLap as invalidateCompetitorLastLapLogic, restoreCompetitorLastInvalidLap as restoreCompetitorLastInvalidLapLogic, findPassingByUndoMarker as findPassingByUndoMarkerLogic, undoJudgingAdjustment as undoJudgingAdjustmentLogic, getJudgeFilteredRows as getJudgeFilteredRowsLogic, getJudgeFilteredLog as getJudgeFilteredLogLogic } from "./judging_logic.js";
const PRODUCT_NAME = "RC RaceCore";
const renderRaceFormatFieldView = (labelKey, hintKey, controlHtml, options = {}) => renderRaceFormatField(labelKey, hintKey, controlHtml, options, { t });
const renderRaceFormatContextCardView = (titleKey, hintKey) => renderRaceFormatContextCard(titleKey, hintKey, { t });
const renderManageStatusBadgeView = (status) => renderManageStatusBadge(status, { t });
@@ -327,8 +329,8 @@ const TRANSLATIONS = {
"ui.theme_dark": "Mörk",
"ui.theme_nord": "Nord",
"ui.theme_light": "Ljus",
"brand.title": "JMK RB RaceController",
"brand.subtitle": "RC Timing System",
"brand.title": "RC RaceCore",
"brand.subtitle": "Live Timing & Race Control",
"ui.no_active_session": "Ingen aktiv session",
"ui.event": "Event",
"ui.decoder_online": "Decoder online",
@@ -1074,7 +1076,9 @@ const TRANSLATIONS = {
"settings.test_audio": "Testa ljud",
"settings.audio_note": "Browsern kräver oftast ett klick först innan ljud/tal tillåts.",
"settings.voice_note": "Rösterna kommer från operativsystemet. På Windows ger Microsofts svenska röst oftast bäst uttal. Ange vid behov ett särskilt uttalsnamn under Förare.",
"settings.branding": "Klubbinfo / PDF",
"settings.branding": "Klubbbranding / UI / PDF",
"settings.branding_note": "Klubbnamn, klubbtext och logotyp används i huvudmenyn, overlay, publik träningsprofil och PDF-export.",
"settings.save_branding": "Spara branding",
"settings.club_name": "Klubbnamn",
"settings.club_tagline": "Klubbtext",
"settings.pdf_footer": "PDF-footer",
@@ -1082,10 +1086,10 @@ const TRANSLATIONS = {
"settings.pdf_theme_classic": "Classic",
"settings.pdf_theme_minimal": "Minimal",
"settings.pdf_theme_motorsport": "Motorsport",
"settings.logo": "Logo / overlay",
"settings.logo": "Klubblogotyp",
"settings.logo_upload": "Ladda logo",
"settings.logo_clear": "Rensa logo",
"settings.logo_note": "Logon visas i overlay. PDF-export försöker bädda in loggan automatiskt via backend.",
"settings.logo_note": "Logotypen visas i huvudmenyn, overlay och publik träningsprofil. PDF-export försöker bädda in den via backend.",
"settings.storage": "Lagring",
"settings.race_presets": "Klubb-presetar",
"settings.race_presets_note": "Exportera eller importera lokala klubb-presetar mellan installationer.",
@@ -1320,7 +1324,7 @@ const TRANSLATIONS = {
"overlay.subtitle": "Extern leaderboard-skärm",
"overlay.no_active": "Ingen aktiv session vald.",
"overlay.last_passings": "Senaste passeringar",
"overlay.window_title": "JMK RB RaceController Overlay",
"overlay.window_title": "RC RaceCore Overlay",
"overlay.mode_leaderboard": "Leaderboard",
"overlay.mode_speaker": "Speaker",
"overlay.mode_results": "Resultat",
@@ -1447,8 +1451,8 @@ const TRANSLATIONS = {
"ui.theme_dark": "Dark",
"ui.theme_nord": "Nord",
"ui.theme_light": "Light",
"brand.title": "JMK RB RaceController",
"brand.subtitle": "RC Timing System",
"brand.title": "RC RaceCore",
"brand.subtitle": "Live Timing & Race Control",
"ui.no_active_session": "No Active Session",
"ui.event": "Event",
"ui.decoder_online": "Decoder Online",
@@ -2194,7 +2198,9 @@ const TRANSLATIONS = {
"settings.test_audio": "Test audio",
"settings.audio_note": "Browsers usually require a click first before sound/speech is allowed.",
"settings.voice_note": "Voices are provided by the operating system. Select a matching local voice and set a separate spoken name under Drivers when pronunciation needs help.",
"settings.branding": "Club Info / PDF",
"settings.branding": "Club branding / UI / PDF",
"settings.branding_note": "Club name, tagline, and logo are used in the main menu, overlays, public training profiles, and PDF exports.",
"settings.save_branding": "Save branding",
"settings.club_name": "Club name",
"settings.club_tagline": "Club tagline",
"settings.pdf_footer": "PDF footer",
@@ -2202,10 +2208,10 @@ const TRANSLATIONS = {
"settings.pdf_theme_classic": "Classic",
"settings.pdf_theme_minimal": "Minimal",
"settings.pdf_theme_motorsport": "Motorsport",
"settings.logo": "Logo / overlay",
"settings.logo": "Club logo",
"settings.logo_upload": "Upload logo",
"settings.logo_clear": "Clear logo",
"settings.logo_note": "The logo is shown in overlay. PDF export attempts to embed the logo automatically via the backend.",
"settings.logo_note": "The logo is shown in the main menu, overlays, and public training profiles. PDF export attempts to embed it via the backend.",
"settings.storage": "Storage",
"settings.race_presets": "Club presets",
"settings.race_presets_note": "Export or import local club presets between installations.",
@@ -2440,7 +2446,7 @@ const TRANSLATIONS = {
"overlay.subtitle": "External leaderboard screen",
"overlay.no_active": "No active session selected.",
"overlay.last_passings": "Recent passings",
"overlay.window_title": "JMK RB RaceController Overlay",
"overlay.window_title": "RC RaceCore Overlay",
"overlay.mode_leaderboard": "Leaderboard",
"overlay.mode_speaker": "Speaker",
"overlay.mode_results": "Results",
@@ -2909,16 +2915,19 @@ function seedDefaultData() {
state.settings.speakerFinishCueEnabled = true;
}
if (!state.settings.clubName) {
state.settings.clubName = "JMK RB RaceController";
const storedClubName = String(state.settings.clubName || "").trim();
if (!storedClubName || ["JMK RB RaceController", "RC RaceCore"].includes(storedClubName)) {
state.settings.clubName = "JMK RB";
}
if (!state.settings.clubTagline) {
state.settings.clubTagline = "RC Timing System";
const storedClubTagline = String(state.settings.clubTagline || "").trim();
if (!storedClubTagline || storedClubTagline === "RC Timing System") {
state.settings.clubTagline = "Live Timing & Race Control";
}
if (!state.settings.pdfFooter) {
state.settings.pdfFooter = "Generated by JMK RB RaceController";
const storedPdfFooter = String(state.settings.pdfFooter || "").trim();
if (!storedPdfFooter || storedPdfFooter === "Generated by JMK RB RaceController") {
state.settings.pdfFooter = "Generated by RC RaceCore";
}
if (!state.settings.pdfTheme) {
@@ -2996,9 +3005,9 @@ function loadState() {
speakerBestLapCueEnabled: parsed.settings?.speakerBestLapCueEnabled !== false,
speakerTop3CueEnabled: parsed.settings?.speakerTop3CueEnabled === true,
speakerSessionStartCueEnabled: parsed.settings?.speakerSessionStartCueEnabled !== false,
clubName: parsed.settings?.clubName || "JMK RB RaceController",
clubTagline: parsed.settings?.clubTagline || "RC Timing System",
pdfFooter: parsed.settings?.pdfFooter || "Generated by JMK RB RaceController",
clubName: parsed.settings?.clubName || "JMK RB",
clubTagline: parsed.settings?.clubTagline || "Live Timing & Race Control",
pdfFooter: parsed.settings?.pdfFooter || "Generated by RC RaceCore",
pdfTheme: parsed.settings?.pdfTheme || "classic",
logoDataUrl: parsed.settings?.logoDataUrl || "",
racePresets: Array.isArray(parsed.settings?.racePresets)
@@ -3062,9 +3071,9 @@ function loadState() {
speakerBestLapCueEnabled: true,
speakerTop3CueEnabled: false,
speakerSessionStartCueEnabled: true,
clubName: "JMK RB RaceController",
clubTagline: "RC Timing System",
pdfFooter: "Generated by JMK RB RaceController",
clubName: "JMK RB",
clubTagline: "Live Timing & Race Control",
pdfFooter: "Generated by RC RaceCore",
pdfTheme: "classic",
logoDataUrl: "",
racePresets: [],
@@ -3143,14 +3152,7 @@ function setupLanguageControl() {
if (label) {
label.textContent = t("ui.language");
}
const brandTitle = document.getElementById("brandTitle");
if (brandTitle) {
brandTitle.textContent = t("brand.title");
}
const brandSubtitle = document.getElementById("brandSubtitle");
if (brandSubtitle) {
brandSubtitle.textContent = t("brand.subtitle");
}
updateAppBranding();
const select = document.getElementById("languageSelect");
if (!(select instanceof HTMLSelectElement)) {
@@ -3168,6 +3170,27 @@ function setupLanguageControl() {
};
}
function updateAppBranding() {
const clubName = String(state.settings?.clubName || "").trim() || PRODUCT_NAME;
const clubTagline = String(state.settings?.clubTagline || "").trim() || t("brand.subtitle");
const logoDataUrl = String(state.settings?.logoDataUrl || "").trim();
const hasLogo = /^data:image\/(?:png|jpeg|webp);base64,/i.test(logoDataUrl);
const brandTitle = document.getElementById("brandTitle");
const brandSubtitle = document.getElementById("brandSubtitle");
const brandMark = document.getElementById("brandMark");
const brandLogo = document.getElementById("brandLogo");
if (brandTitle) brandTitle.textContent = clubName;
if (brandSubtitle) brandSubtitle.textContent = clubTagline;
if (brandMark) brandMark.classList.toggle("has-logo", hasLogo);
if (brandLogo instanceof HTMLImageElement) {
brandLogo.hidden = !hasLogo;
if (hasLogo) brandLogo.src = logoDataUrl;
else brandLogo.removeAttribute("src");
}
document.title = clubName === PRODUCT_NAME ? PRODUCT_NAME : `${clubName} | ${PRODUCT_NAME}`;
}
function buildPersistableState() {
syncTrainingLedger();
return {
@@ -3193,7 +3216,7 @@ function normalizeImportedClass(classItem) {
function buildExportMeta(exportType) {
return {
app: "JMK RB RaceController",
app: "RC RaceCore",
schemaVersion: EXPORT_SCHEMA_VERSION,
exportType,
exportedAt: new Date().toISOString(),
@@ -3571,6 +3594,7 @@ function updateViewUrl(view, driverId = "", classId = "") {
}
function renderView() {
updateAppBranding();
if (currentView === "settings") settingsDraftActive = false;
clearModalEscapeHandler();
const navMeta = NAV_ITEMS.find((x) => x.id === currentView);

View File

@@ -257,7 +257,7 @@ export function resolveEventBranding(event, { normalizeBrandingConfig, state })
return {
brandName: local.brandName || state.settings.clubName || "JMK RB",
brandTagline: local.brandTagline || state.settings.clubTagline || "Live Event",
pdfFooter: local.pdfFooter || state.settings.pdfFooter || "Generated by JMK RB RaceController",
pdfFooter: local.pdfFooter || state.settings.pdfFooter || "Generated by RC RaceCore",
pdfTheme: local.pdfTheme || state.settings.pdfTheme || "classic",
logoDataUrl: local.logoDataUrl || state.settings.logoDataUrl || "",
};

View File

@@ -155,7 +155,7 @@ export function renderOverlayPageView(deps) {
<span class="pill">${escapeHtml(modeLabel)}</span>
</div>
<h1>${escapeHtml(active.name)}</h1>
<p class="overlay-header-sub">${escapeHtml(branding.brandName || "JMK RB RaceController")}</p>
<p class="overlay-header-sub">${escapeHtml(branding.brandName || "RC RaceCore")}</p>
</div>
</div>
<div class="overlay-meta">

View File

@@ -18,6 +18,11 @@ export function createDefaultAmmcConfigHelper() {
};
}
function migrateBrandDefault(value, legacyValues, fallback) {
const normalized = String(value || "").trim();
return !normalized || legacyValues.includes(normalized) ? fallback : normalized;
}
export function getManagedWsUrlHelper({ ammc, getBackendUrl }, role = "main") {
const portKey = role === "split" ? "splitWsPort" : role === "pit_entry" ? "pitEntryWsPort" : role === "pit_exit" ? "pitExitWsPort" : "wsPort";
const defaultPort = role === "split" ? 9001 : role === "pit_entry" ? 9002 : role === "pit_exit" ? 9003 : 9000;
@@ -131,9 +136,9 @@ export function applyPersistedStateHelper(persisted, deps) {
speakerBestLapCueEnabled: persisted.settings?.speakerBestLapCueEnabled !== false,
speakerTop3CueEnabled: persisted.settings?.speakerTop3CueEnabled === true,
speakerSessionStartCueEnabled: persisted.settings?.speakerSessionStartCueEnabled !== false,
clubName: persisted.settings?.clubName || state.settings.clubName || "JMK RB RaceController",
clubTagline: persisted.settings?.clubTagline || state.settings.clubTagline || "RC Timing System",
pdfFooter: persisted.settings?.pdfFooter || state.settings.pdfFooter || "Generated by JMK RB RaceController",
clubName: migrateBrandDefault(persisted.settings?.clubName || state.settings.clubName, ["JMK RB RaceController", "RC RaceCore"], "JMK RB"),
clubTagline: migrateBrandDefault(persisted.settings?.clubTagline || state.settings.clubTagline, ["RC Timing System"], "Live Timing & Race Control"),
pdfFooter: migrateBrandDefault(persisted.settings?.pdfFooter || state.settings.pdfFooter, ["Generated by JMK RB RaceController"], "Generated by RC RaceCore"),
pdfTheme: persisted.settings?.pdfTheme || state.settings.pdfTheme || "classic",
logoDataUrl: persisted.settings?.logoDataUrl || state.settings.logoDataUrl || "",
raceAudio: normalizeRaceAudioSettings(persisted.settings?.raceAudio || state.settings?.raceAudio),

View File

@@ -272,15 +272,18 @@ export function renderSettings(deps) {
<section class="panel mt-16">
<div class="panel-header"><h3>${t("settings.branding")}</h3></div>
<div class="panel-body">
<p class="hint">${t("settings.branding_note")}</p>
</div>
<div class="panel-body form-grid cols-3 settings-grid">
<input name="clubName" form="settingsForm" value="${escapeHtml(state.settings.clubName || "")}" placeholder="${t("settings.club_name")}" />
<input name="clubTagline" form="settingsForm" value="${escapeHtml(state.settings.clubTagline || "")}" placeholder="${t("settings.club_tagline")}" />
<input name="pdfFooter" form="settingsForm" value="${escapeHtml(state.settings.pdfFooter || "")}" placeholder="${t("settings.pdf_footer")}" />
<select name="pdfTheme" form="settingsForm">
<label><span>${t("settings.club_name")}</span><input name="clubName" form="settingsForm" value="${escapeHtml(state.settings.clubName || "")}" placeholder="JMK RB" /></label>
<label><span>${t("settings.club_tagline")}</span><input name="clubTagline" form="settingsForm" value="${escapeHtml(state.settings.clubTagline || "")}" placeholder="Live Timing & Race Control" /></label>
<label><span>${t("settings.pdf_footer")}</span><input name="pdfFooter" form="settingsForm" value="${escapeHtml(state.settings.pdfFooter || "")}" placeholder="Generated by RC RaceCore" /></label>
<label><span>${t("settings.pdf_theme")}</span><select name="pdfTheme" form="settingsForm">
<option value="classic" ${state.settings.pdfTheme === "classic" ? "selected" : ""}>${t("settings.pdf_theme_classic")}</option>
<option value="minimal" ${state.settings.pdfTheme === "minimal" ? "selected" : ""}>${t("settings.pdf_theme_minimal")}</option>
<option value="motorsport" ${state.settings.pdfTheme === "motorsport" ? "selected" : ""}>${t("settings.pdf_theme_motorsport")}</option>
</select>
</select></label>
</div>
<div class="panel-body">
<h4>${t("settings.logo")}</h4>
@@ -288,6 +291,7 @@ export function renderSettings(deps) {
<div class="actions">
<input id="logoUpload" type="file" accept="image/*" />
<button id="clearLogo" class="btn" type="button">${t("settings.logo_clear")}</button>
<button class="btn btn-primary" type="submit" form="settingsForm">${t("settings.save_branding")}</button>
</div>
${
state.settings.logoDataUrl
@@ -567,9 +571,9 @@ export function renderSettings(deps) {
state.settings.speakerBestLapCueEnabled = form.get("speakerBestLapCueEnabled") === "on";
state.settings.speakerTop3CueEnabled = form.get("speakerTop3CueEnabled") === "on";
state.settings.speakerSessionStartCueEnabled = form.get("speakerSessionStartCueEnabled") === "on";
state.settings.clubName = String(form.get("clubName") || "").trim() || "JMK RB RaceController";
state.settings.clubTagline = String(form.get("clubTagline") || "").trim() || "RC Timing System";
state.settings.pdfFooter = String(form.get("pdfFooter") || "").trim() || "Generated by JMK RB RaceController";
state.settings.clubName = String(form.get("clubName") || "").trim() || "JMK RB";
state.settings.clubTagline = String(form.get("clubTagline") || "").trim() || "Live Timing & Race Control";
state.settings.pdfFooter = String(form.get("pdfFooter") || "").trim() || "Generated by RC RaceCore";
state.settings.pdfTheme = ["classic", "minimal", "motorsport"].includes(String(form.get("pdfTheme") || "classic"))
? String(form.get("pdfTheme"))
: "classic";

View File

@@ -122,6 +122,7 @@ body {
}
.brand-mark {
overflow: hidden;
width: 46px;
height: 46px;
border-radius: 10px;
@@ -3216,3 +3217,20 @@ select:focus {
.audio-event-table td:first-child { min-width: 170px; }
.race-control-cues .btn { flex: 1 1 140px; }
}
.brand-mark.has-logo {
padding: 3px;
background: var(--surface-card);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
}
.brand-mark.has-logo > span {
display: none;
}
.brand-logo {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 7px;
}