Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 147 additions & 19 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,58 @@ const calendarHistoryState = {
summaryMeta: null,
};

const RANDOMIZE_SEED_STORAGE_KEY = 'cfg.calendar.randomizeSeed';
let randomizeSeed = false;
let randomizeSeedToggle = null;
let seedIndicatorElement = null;
let calendarConfigController = null;

function readRandomizeSeedPreference() {
try {
return localStorage.getItem(RANDOMIZE_SEED_STORAGE_KEY) === '1';
} catch (error) {
return false;
}
}

function persistRandomizeSeedPreference(enabled) {
try {
localStorage.setItem(RANDOMIZE_SEED_STORAGE_KEY, enabled ? '1' : '0');
} catch (error) {
// ignore storage failures
}
}

function updateRandomizeSeedToggleUI() {
if (randomizeSeedToggle) {
randomizeSeedToggle.checked = Boolean(randomizeSeed);
}
}

function updateSeedIndicator(seedValue) {
if (!seedIndicatorElement) {
return;
}
let value = seedValue;
if (typeof value === 'undefined') {
if (calendarConfigState && calendarConfigState.common) {
value = calendarConfigState.common.seed;
} else if (typeof getConfigSnapshot === 'function') {
try {
const snapshot = getConfigSnapshot();
value = snapshot?.seed;
} catch (error) {
value = undefined;
}
}
}
if (value === null || typeof value === 'undefined' || value === '') {
seedIndicatorElement.textContent = '—';
return;
}
seedIndicatorElement.textContent = String(value);
}

function readVisualsLegacyFlag() {
try {
return localStorage.getItem(VISUALS_LEGACY_FLAG) === '1';
Expand Down Expand Up @@ -939,6 +991,13 @@ function renderRunHistory() {
? ''
: `${eventCount} event${eventCount === 1 ? '' : 's'}`;
const metaParts = [`${timestamp}`, `Week: ${week}`];
const seedDisplay =
entry.seed !== undefined && entry.seed !== null && entry.seed !== ''
? String(entry.seed)
: '';
if (seedDisplay) {
metaParts.push(`Seed: ${seedDisplay}`);
}
if (eventText) {
metaParts.push(eventText);
}
Expand Down Expand Up @@ -1007,6 +1066,12 @@ function addRunHistoryEntry(entry) {
class: entry.class || 'calendar',
variant: entry.variant || '',
rig: entry.rig || '',
seed:
typeof entry.seed === 'number'
? entry.seed
: typeof entry.seed === 'string' && entry.seed.trim().length > 0
? entry.seed.trim()
: '',
week_start: typeof entry.week_start === 'string' ? entry.week_start : '',
label: entry.label || '',
payload: clonePayload(entry.payload) || entry.payload || {},
Expand Down Expand Up @@ -2230,6 +2295,15 @@ function setGenerateButtonState(loading) {
updateGenerateButtonDisabledForRuntime();
}

function handleRandomizeSeedToggleChange() {
if (!randomizeSeedToggle) {
return;
}
randomizeSeed = randomizeSeedToggle.checked;
persistRandomizeSeedPreference(randomizeSeed);
updateRandomizeSeedToggleUI();
}

async function handleGenerate(event) {
if (event && typeof event.preventDefault === 'function') {
event.preventDefault();
Expand All @@ -2238,18 +2312,39 @@ async function handleGenerate(event) {
return;
}

let randomizedSeedText = '';
if (randomizeSeed) {
randomizedSeedText = String(Math.floor(Math.random() * 1_000_000_000));
let appliedSeed = null;
if (calendarConfigController && typeof calendarConfigController.setSeed === 'function') {
appliedSeed = calendarConfigController.setSeed(randomizedSeedText, {
markUserEdited: true,
persist: true,
});
if (typeof appliedSeed === 'string' && appliedSeed.trim().length > 0) {
randomizedSeedText = appliedSeed.trim();
}
}
if (!appliedSeed && calendarConfigState && calendarConfigState.common) {
calendarConfigState.common.seed = randomizedSeedText;
updateSeedIndicator(randomizedSeedText);
}
}

const snapshot = typeof getConfigSnapshot === 'function' ? getConfigSnapshot() : {};
const variantId = snapshot.variant || 'mk1';
const rigId = snapshot.rig || 'default';
const config = calendarConfigState || {};
const archetype = config?.common?.archetype || '';
const weekStartValue = snapshot.week_start || '';
const seedValue = snapshot.seed || '';
const seedValue = snapshot.seed || randomizedSeedText;
const budgetText =
variantId === 'mk2' && rigId === 'workforce'
? config?.mk2?.workforce?.budgetText || ''
: '';

updateSeedIndicator(seedValue);

setGenerateButtonState(true);
updateVisuals(null);
showVisualsOverlay('Generating schedule…', { loading: true });
Expand Down Expand Up @@ -2352,6 +2447,7 @@ async function handleGenerate(event) {
? result.week_start
: weekStartValue,
label: 'Generated schedule',
seed: normalizedSeed,
payload: result,
inputs: inputsSnapshot,
resultSummary: { events: eventsCount },
Expand Down Expand Up @@ -3170,6 +3266,22 @@ function hydrateGlobalActions() {
if (!(button instanceof HTMLElement)) {
return;
}
seedIndicatorElement = actionsRoot.querySelector('[data-current-seed]') || seedIndicatorElement;
if (!actionsRoot.dataset.randomizeSeedHydrated) {
randomizeSeed = readRandomizeSeedPreference();
actionsRoot.dataset.randomizeSeedHydrated = '1';
}
const toggle = actionsRoot.querySelector('#randomize-seed-toggle');
if (toggle !== randomizeSeedToggle) {
if (randomizeSeedToggle) {
randomizeSeedToggle.removeEventListener('change', handleRandomizeSeedToggleChange);
}
randomizeSeedToggle = toggle instanceof HTMLInputElement ? toggle : null;
if (randomizeSeedToggle) {
randomizeSeedToggle.addEventListener('change', handleRandomizeSeedToggleChange);
}
}
updateRandomizeSeedToggleUI();
if (generateButton && generateButton !== button) {
generateButton.removeEventListener('click', handleGenerate);
}
Expand All @@ -3180,6 +3292,7 @@ function hydrateGlobalActions() {
button.addEventListener('click', handleGenerate);
button.dataset.generateBound = '1';
}
updateSeedIndicator();
}

function hydrateConfigPanel() {
Expand Down Expand Up @@ -3365,6 +3478,10 @@ function hydrateConfigPanel() {
},
};
calendarConfigState = calendarConfig;
calendarConfigController = {
setSeed: (value, options) => setCalendarSeed(value, options),
getSeed: () => (calendarConfig.common.seed ? String(calendarConfig.common.seed) : ''),
};

let commonSeedUserEdited = false;
let shouldPersistInitialCommon = false;
Expand Down Expand Up @@ -3521,6 +3638,7 @@ function hydrateConfigPanel() {
cfg.weekStart = calendarConfig.common.weekStart || '';
cfg.seed = calendarConfig.common.seed ? String(calendarConfig.common.seed) : '';
renderSummaryChips();
updateSeedIndicator(calendarConfig.common.seed);
};

const computeDeterministicSeed = () => {
Expand All @@ -3545,6 +3663,7 @@ function hydrateConfigPanel() {
if (seedInput) {
seedInput.value = calendarConfig.common.seed || computeDeterministicSeed();
}
updateSeedIndicator(calendarConfig.common.seed);
};

const persistCalendarCommon = () => {
Expand All @@ -3558,6 +3677,21 @@ function hydrateConfigPanel() {
persistJsonStorage(CALENDAR_COMMON_STORAGE_KEY, payload);
};

function setCalendarSeed(nextSeed, { markUserEdited = true, persist = true } = {}) {
const parsedSeed = Number.parseInt(String(nextSeed), 10);
if (!Number.isFinite(parsedSeed)) {
return '';
}
calendarConfig.common.seed = String(parsedSeed);
commonSeedUserEdited = Boolean(markUserEdited);
if (persist) {
persistCalendarCommon();
}
syncCommonInputs();
syncCommonStateToChips();
return calendarConfig.common.seed;
}

const persistCalendarMk2Calendar = () => {
persistJsonStorage(CALENDAR_MK2_CALENDAR_STORAGE_KEY, {
compress: Boolean(calendarConfig.mk2.calendar.compress),
Expand Down Expand Up @@ -3651,38 +3785,32 @@ function hydrateConfigPanel() {
seedInput.addEventListener('change', () => {
const value = seedInput.value.trim();
if (!value) {
commonSeedUserEdited = false;
calendarConfig.common.seed = computeDeterministicSeed();
seedInput.value = calendarConfig.common.seed;
} else {
const numericSeed = Number.parseInt(value, 10);
if (!Number.isFinite(numericSeed)) {
seedInput.value = calendarConfig.common.seed || computeDeterministicSeed();
return;
}
calendarConfig.common.seed = String(numericSeed);
commonSeedUserEdited = true;
setCalendarSeed(computeDeterministicSeed(), { markUserEdited: false, persist: true });
updateDerivedFromFoundation();
return;
}
persistCalendarCommon();
const numericSeed = Number.parseInt(value, 10);
if (!Number.isFinite(numericSeed)) {
seedInput.value = calendarConfig.common.seed || computeDeterministicSeed();
updateSeedIndicator(calendarConfig.common.seed);
return;
}
setCalendarSeed(numericSeed, { markUserEdited: true, persist: true });
updateDerivedFromFoundation();
});
}

if (seedResetButton) {
seedResetButton.addEventListener('click', () => {
commonSeedUserEdited = false;
calendarConfig.common.seed = computeDeterministicSeed();
persistCalendarCommon();
setCalendarSeed(computeDeterministicSeed(), { markUserEdited: false, persist: true });
updateDerivedFromFoundation();
});
}

if (seedRandomButton) {
seedRandomButton.addEventListener('click', () => {
const randomSeed = Math.floor(Math.random() * 1_000_000_000);
calendarConfig.common.seed = String(randomSeed);
commonSeedUserEdited = true;
persistCalendarCommon();
setCalendarSeed(randomSeed, { markUserEdited: true, persist: true });
updateDerivedFromFoundation();
});
}
Expand Down
16 changes: 13 additions & 3 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,19 @@
</button>
</nav>
<div class="workbench-toolbar" data-global-actions>
<button type="button" data-global-action="generate">
Generate schedule
</button>
<div class="toolbar-info">
<span class="toolbar-seed-label">Seed:</span>
<span class="toolbar-seed-value" data-current-seed>—</span>
</div>
<div class="toolbar-actions">
<label class="toolbar-toggle" for="randomize-seed-toggle">
<input type="checkbox" id="randomize-seed-toggle" />
<span>Randomise seed</span>
</label>
<button type="button" data-global-action="generate">
Generate schedule
</button>
</div>
</div>
<main class="tab-content">
<section
Expand Down
35 changes: 35 additions & 0 deletions web/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,41 @@ body {
border-bottom: 1px solid #2a2d35;
}

.toolbar-info {
display: inline-flex;
align-items: center;
gap: 6px;
margin-right: auto;
font-size: 12px;
color: #9ea3b6;
}

.toolbar-seed-value {
font-variant-numeric: tabular-nums;
color: #e5e7eb;
font-weight: 600;
}

.toolbar-actions {
display: inline-flex;
align-items: center;
gap: 12px;
}

.toolbar-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #cbd0df;
cursor: pointer;
user-select: none;
}

.toolbar-toggle input {
margin: 0;
}

.tab {
flex: 1;
background: #1f2128;
Expand Down