diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b5bbcf..301c7e0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@ The application version is maintained in `package.json` and displayed in the adm
No changes recorded yet.
+## 1.17.0 - 2026-09-11
+
+### Changed
+
+- Event Manager's race-mode page (a long scroll of participants, teams, race format, live grid/standings, schedule and print/PDF tools) is now split into four tabs - Participants & teams, Format, Live & results, Schedule & print - so a specific section is a click away. The existing step-card overview and "fix this" warning links now switch to the right tab automatically before jumping to it. Track/sponsor events, which only ever had a handful of panels, keep the simple untabbed layout.
+
## 1.16.1 - 2026-09-10
### Fixed
diff --git a/ROADMAP.md b/ROADMAP.md
index 4a49b98..46022a4 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -397,6 +397,18 @@ That second finding was real and worth fixing on its own even though it didn't m
Verified 2026-09-10: extended the existing permanent browser test (`tests/browser_race_day_start_sequences.js`) with a "practice"-type session run through the same full-ceremony assertions already used for qualification/final/team_race (real audio events logged, pause/resume/finish all work), plus a new explicit check that "open_practice" (the Quick Open Practice shortcut) still jumps straight to running with zero new audio events logged - locking in both sides of the distinction so this doesn't regress either direction. Full unit and browser regression suites re-run clean.
+## Event Manager: tabbed layout for race-mode events
+
+Requested 2026-09-11 by the user, alongside a Guide update, as the second of two "make it easier for a new, non-technical person to use RaceCore" asks. Event Manager's race-mode page had grown into a long scroll: participants, teams, race format (practice/finals/rules + summary + generate/reseed/bump/export), live grid + four standings tables + channel analysis, and (since the schedule feature shipped this session) a Schedule panel plus the print/PDF Final Matrix - the same kind of length problem already fixed for the Settings page and the session picker earlier this session.
+
+- [x] Wrapped the existing race-mode content (`src/event_manager_view.js`) into four `
` groups - `setup` (participants + teams), `format` (the whole race-format section, generation buttons included), `live` (grid editor + all standings + channel analysis), `schedule` (the Schedule panel + Final Matrix/print/PDF) - with a `.manage-tabs` bar to switch between them, mirroring the Settings tabs pattern (`.manage-tab`/`.manage-tab.active`, `[data-manage-panel][hidden]`). No section's own markup, ids, or event bindings moved - only wrapper divs were added, all contiguous, since this whole part of the page already lives inside a single `event.mode === "race"` conditional block.
+- [x] The session-plan (create/list sessions) and branding sections, which apply to every event mode and sit above this block, stay always visible regardless of the active tab - as does the existing 4-card step overview (Setup/Format/Generate/Live), which is a permanent summary rather than a fifth tab.
+- [x] The step-card overview's jump links and the race-summary's "fix this" warning links (both pre-existing, previously just `scrollIntoView`) now switch to the correct tab first via a small `MANAGE_TAB_TARGETS` lookup (`src/event_manager_controller.js`) - scrolling to an element hidden behind an inactive tab previously did nothing useful. The "Open Grid" button (session list) also now switches to the Live tab so the grid editor it opens is actually visible.
+- [x] Track/sponsor events don't enter this conditional block at all (they only ever had session-plan, branding, sponsor tools and assignments - never the length problem), so they keep the exact same untabbed layout as before, unchanged.
+- [x] Active tab is held in module-scoped state (`activeManageTab` in `src/event_manager_controller.js`, same approach as Settings' `activeSettingsTab`), so it survives re-renders from other actions on the page without snapping back to the first tab.
+
+Verified 2026-09-11 with a live headless-browser check against a real, isolated server: confirmed the Setup tab is shown by default with the other three hidden; clicking each tab shows that tab's own content (format's `generateQualifying` button, live's grid editor, schedule's form and print buttons) while hiding the others; confirmed session-plan and branding stay visible on every tab; confirmed a step-card jump to a Live-tab target both switches to that tab and makes the target actually visible (not just scrolled-to-while-hidden); confirmed the Open Grid button switches to the Live tab; and confirmed a sponsor/track-mode event's manager page renders with zero `.manage-tab` elements, i.e. its layout is untouched. The full existing browser regression suite (23 tests, including the sponsor-event and race-wizard flows that exercise this same page) was also re-run and passed unchanged.
+
## Documentation maintenance
- [ ] Update the architecture's module/dependency inventory as modules change.
diff --git a/package-lock.json b/package-lock.json
index 05a19fc..572bd4a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "rc-racecore",
- "version": "1.16.1",
+ "version": "1.17.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rc-racecore",
- "version": "1.16.1",
+ "version": "1.17.0",
"hasInstallScript": true,
"dependencies": {
"better-sqlite3": "^12.11.1",
diff --git a/package.json b/package.json
index 095912b..bce1e0c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "rc-racecore",
- "version": "1.16.1",
+ "version": "1.17.0",
"private": true,
"description": "RC RaceCore live timing and race control with AMMC ingest and SQLite persistence",
"engines": {
diff --git a/src/app.js b/src/app.js
index 07f8a86..55c62e1 100644
--- a/src/app.js
+++ b/src/app.js
@@ -965,7 +965,11 @@ const TRANSLATIONS = {
"events.manage_step_generate": "3. Generering",
"events.manage_step_generate_hint": "Skapa kval/finaler, reseeda och bump-up.",
"events.manage_step_live": "4. Live / resultat",
- "events.manage_step_live_hint": "Grid, standings, print och PDF.",
+ "events.manage_step_live_hint": "Grid och standings.",
+ "events.manage_tab_setup": "Deltagare & lag",
+ "events.manage_tab_format": "Format",
+ "events.manage_tab_live": "Live & resultat",
+ "events.manage_tab_schedule": "Schema & utskrift",
"events.branding": "Branding för detta event",
"events.branding_note": "Lämna fält tomma för att ärva global branding från Inställningar. Logo används i overlay och bäddas in i PDF-export när den kan konverteras.",
"events.brand_name": "Brandnamn",
@@ -2493,7 +2497,11 @@ const TRANSLATIONS = {
"events.manage_step_generate": "3. Generation",
"events.manage_step_generate_hint": "Generate qualifying/finals, reseed and bump-up.",
"events.manage_step_live": "4. Live / results",
- "events.manage_step_live_hint": "Grid, standings, print and PDF.",
+ "events.manage_step_live_hint": "Grid and standings.",
+ "events.manage_tab_setup": "Participants & teams",
+ "events.manage_tab_format": "Format",
+ "events.manage_tab_live": "Live & results",
+ "events.manage_tab_schedule": "Schedule & print",
"events.branding": "Branding for this event",
"events.branding_note": "Leave fields empty to inherit global branding from Settings. The logo is used in overlay and embedded in PDF exports when it can be converted.",
"events.brand_name": "Brand name",
diff --git a/src/event_manager_controller.js b/src/event_manager_controller.js
index 6236dd5..333c50d 100644
--- a/src/event_manager_controller.js
+++ b/src/event_manager_controller.js
@@ -1,3 +1,16 @@
+let activeManageTab = "setup";
+// Maps a data-target anchor id (used by the step-card overview and the race-summary
+// warning links) to the tab it now lives inside, so jumping to it also switches tabs
+// first - scrollIntoView on an element hidden behind an inactive tab does nothing useful.
+const MANAGE_TAB_TARGETS = {
+ "manage-setup-participants": "setup",
+ "manage-setup-teams": "setup",
+ "manage-format": "format",
+ "manage-generation": "format",
+ "manage-live-grid": "live",
+ "manage-live-results": "live",
+};
+
export function renderEventManagerView(context) {
const {
eventId,
@@ -201,9 +214,25 @@ export function renderEventManagerView(context) {
renderTable,
});
+ const manageTabButtons = Array.from(eventManageArea.querySelectorAll(".manage-tab"));
+ const manageTabPanels = Array.from(eventManageArea.querySelectorAll("[data-manage-panel]"));
+ const applyManageTab = (tabId) => {
+ activeManageTab = tabId;
+ manageTabButtons.forEach((button) => button.classList.toggle("active", button.dataset.manageTab === tabId));
+ manageTabPanels.forEach((panel) => { panel.hidden = panel.dataset.managePanel !== tabId; });
+ };
+ manageTabButtons.forEach((button) => {
+ button.addEventListener("click", () => applyManageTab(button.dataset.manageTab));
+ });
+ if (manageTabButtons.length) {
+ applyManageTab(manageTabButtons.some((button) => button.dataset.manageTab === activeManageTab) ? activeManageTab : "setup");
+ }
+
const bindManageJump = (node) => {
const triggerJump = () => {
const targetId = node.getAttribute("data-target") || "";
+ const targetTab = MANAGE_TAB_TARGETS[targetId];
+ if (targetTab) applyManageTab(targetTab);
const target = targetId ? document.getElementById(targetId) : null;
if (target) {
target.scrollIntoView({ behavior: "smooth", block: "start" });
@@ -344,6 +373,7 @@ export function renderEventManagerView(context) {
document.getElementById(`session-grid-${s.id}`)?.addEventListener("click", () => {
ensureSessionDriverOrder(s);
setSelectedGridSessionId(s.id);
+ activeManageTab = "live";
saveState();
refreshManager();
});
diff --git a/src/event_manager_view.js b/src/event_manager_view.js
index 27129e5..181dfda 100644
--- a/src/event_manager_view.js
+++ b/src/event_manager_view.js
@@ -300,6 +300,14 @@ export function renderEventManagerMarkup(context) {