`;
// Display each state with color coding
const stateColors = {
completed: '#065f46',
live: '#dc2626',
ready: '#ca8a04',
pending: '#666'
};
for (const [state, matchIds] of Object.entries(matchesByState)) {
const count = matchIds.length;
const stateName = state.toUpperCase();
const color = stateColors[state] || '#666';
html += `
${stateName}: ${count} matches
`;
if (count > 0) {
html += `
${matchIds.join(', ')}
`;
}
html += `
`;
}
html += `
`;
updateRightPane(html);
}
/**
* Show Transaction History
*/
function showTransactionHistory(filterType = 'all', filterMatchId = '', filterSearch = '') {
currentView = 'history';
const history = getTournamentHistory ? getTournamentHistory() : [];
// Add original transaction numbers before filtering
// History is stored newest first, so index 0 = newest transaction
const historyWithNumbers = history.map((tx, idx) => ({
...tx,
originalNumber: history.length - idx // #N for newest, #1 for oldest
}));
// Apply filters
let filteredHistory = [...historyWithNumbers];
// Filter by type
if (filterType && filterType !== 'all') {
filteredHistory = filteredHistory.filter(tx => tx.type === filterType);
}
// Filter by match ID
if (filterMatchId && filterMatchId.trim() !== '') {
const matchIdLower = filterMatchId.trim().toLowerCase();
filteredHistory = filteredHistory.filter(tx =>
(tx.matchId && tx.matchId.toLowerCase().includes(matchIdLower)) ||
(tx.description && tx.description.toLowerCase().includes(matchIdLower))
);
}
// Filter by search text
if (filterSearch && filterSearch.trim() !== '') {
const searchLower = filterSearch.trim().toLowerCase();
filteredHistory = filteredHistory.filter(tx =>
tx.description && tx.description.toLowerCase().includes(searchLower)
);
}
const totalCount = history.length;
const filteredCount = filteredHistory.length;
const countText = filteredCount < totalCount
? `${filteredCount} of ${totalCount} total`
: `${totalCount} total`;
let html = `
Transaction History (${countText})
`;
if (filteredHistory.length === 0) {
if (totalCount === 0) {
html += `
No transactions recorded
`;
} else {
html += `
No transactions match filters
`;
}
} else {
// Display transactions with their original numbers preserved (even when filtered)
// Transactions are already in newest-first order from storage
filteredHistory.forEach((tx) => {
const time = new Date(tx.timestamp).toLocaleTimeString();
html += `
This puts the Global Settings back to their defaults. The server connection is kept (the server address, API key and server ID). Your tournament data (matches, players, brackets, history) is not affected.
What will change (${changes.length} setting${changes.length === 1 ? '' : 's'}):
${comparisonHtml}
To confirm this action:
Type RESET in the box below and click "Reset Config"
`;
updateRightPane(html);
}
/**
* Execute Reset All Config (called from confirmation button)
*/
function executeResetAllConfig() {
const confirmInput = document.getElementById('resetConfirmInput');
if (!confirmInput || confirmInput.value.trim().toUpperCase() !== 'RESET') {
alert('Please type RESET to confirm');
return;
}
console.log('Resetting all config to defaults...');
if (typeof defaultConfigKeepingConnection !== 'function') {
console.error('The default settings are not available');
showCommandFeedback('Reset All Config', 'error', 'The default settings are not available');
return;
}
try {
const changed = configChangesOnReset().length;
// Reset to the defaults, keeping the server connection (defaultConfigKeepingConnection())
config = defaultConfigKeepingConnection();
// Save to localStorage
if (typeof saveGlobalConfig === 'function') {
saveGlobalConfig();
} else {
localStorage.setItem('dartsConfig', JSON.stringify(config));
}
console.log('✓ Config reset to defaults');
console.log('✓ Saved to localStorage');
// Build success feedback with comparison
let feedback = 'Settings reset to their defaults. The page will reload in 2 seconds to apply them.\n\n';
feedback += `• ${changed} setting${changed === 1 ? '' : 's'} changed\n`;
feedback += '• The server connection is kept\n';
showCommandFeedback('Reset All Config', 'success', feedback);
// Auto-refresh after 2 seconds
setTimeout(() => {
console.log('Reloading page to apply config changes...');
location.reload();
}, 2000);
} catch (error) {
console.error('❌ Error resetting config:', error);
showCommandFeedback('Reset All Config', 'error', `Error resetting config: ${error.message}`);
}
}
/**
* Command: Toggle Read-Only Status
*/
function commandToggleReadOnly() {
if (!tournament) {
showCommandFeedback('Toggle Read-Only', 'error', 'No active tournament');
return;
}
const wasReadOnly = tournament.readOnly || false;
const newReadOnly = !wasReadOnly;
console.log(`Toggling read-only status: ${wasReadOnly} → ${newReadOnly}`);
// Toggle the read-only flag
tournament.readOnly = newReadOnly;
// Save the tournament with new status
if (typeof saveTournament === 'function') {
saveTournament();
}
// Re-render bracket to update match icons (checkmark ✓ vs undo ↻)
if (typeof renderBracket === 'function') {
renderBracket();
}
const statusText = newReadOnly ? 'READ-ONLY' : 'READ-WRITE';
const statusColor = newReadOnly ? '#dc2626' : '#059669';
const icon = newReadOnly ? '🔒' : '🔓';
console.log(`✓ Tournament is now ${statusText}`);
showCommandFeedback('Toggle Read-Only', 'success',
`${icon} Tournament set to ${statusText}\n\n${newReadOnly ?
'• Undo operations disabled\n• Match modifications disabled\n• Tournament data protected' :
'• Undo operations enabled\n• Match modifications enabled\n• Tournament fully editable'}`);
}
// ============================================================================
// Command: Late Registration
// ============================================================================
/** Module-level state persisted between Late Registration steps. */
let lateRegState = null;
/**
* Helper: scans FS Round 1 BYE slots and returns eligibility results.
* Used by both the eligibility display step and the execution step.
*
* @returns {{eligible: string[], ineligible: Array<{matchId: string, blocking: string[]}>, total: number} | null}
*/
function runLateRegistrationEligibility() {
if (!tournament || !matches || matches.length === 0) return null;
const progressionTable = getProgressionTable();
const history = getTournamentHistory();
const byeMatches = matches.filter(m =>
m.id && /^FS-1-\d+$/.test(m.id) &&
(m.player1?.isBye || m.player2?.isBye)
);
if (byeMatches.length === 0) return { eligible: [], ineligible: [], total: 0 };
const eligible = [];
const ineligible = [];
byeMatches.forEach(byeMatch => {
const progression = progressionTable && progressionTable[byeMatch.id];
const blockingMatches = [];
if (progression) {
const formatBlockingMatch = (targetMatchId, isLive) => {
const m = matches.find(x => x.id === targetMatchId);
const players = m ? `${m.player1?.name || '?'} vs ${m.player2?.name || '?'}` : '';
return isLive ? `${targetMatchId} (${players}, live)` : `${targetMatchId} (${players})`;
};
if (progression.winner) {
const [targetMatchId] = progression.winner;
const targetMatch = matches.find(m => m.id === targetMatchId);
if (targetMatch && targetMatch.active) {
blockingMatches.push(formatBlockingMatch(targetMatchId, true));
} else if (targetMatch && targetMatch.completed) {
const targetTransaction = history.find(t => t.matchId === targetMatchId && t.type === 'COMPLETE_MATCH' && t.completionType === 'MANUAL');
if (targetTransaction) blockingMatches.push(formatBlockingMatch(targetMatchId, false));
}
}
if (progression.loser) {
const [targetMatchId] = progression.loser;
const targetMatch = matches.find(m => m.id === targetMatchId);
if (targetMatch && targetMatch.active) {
blockingMatches.push(formatBlockingMatch(targetMatchId, true));
} else if (targetMatch && targetMatch.completed) {
const targetTransaction = history.find(t => t.matchId === targetMatchId && t.type === 'COMPLETE_MATCH' && t.completionType === 'MANUAL');
if (targetTransaction) blockingMatches.push(formatBlockingMatch(targetMatchId, false));
}
}
}
if (blockingMatches.length === 0) {
eligible.push(byeMatch.id);
} else {
ineligible.push({ matchId: byeMatch.id, blocking: blockingMatches });
}
});
return { eligible, ineligible, total: byeMatches.length };
}
/**
* Command: Late Registration — entry point.
* Runs eligibility check and, if slots are available, shows player name input form.
*/
function commandLateRegistration() {
if (!tournament || !matches || matches.length === 0) {
showCommandFeedback('Late Registration', 'error', 'No active tournament.');
return;
}
const result = runLateRegistrationEligibility();
if (!result || result.total === 0) {
showCommandFeedback('Late Registration', 'error',
'No BYE slots found in Frontside Round 1.\n\nLate Registration is not possible for this tournament.');
return;
}
const { eligible, ineligible, total } = result;
const eligibleCount = eligible.length;
let message = `${eligibleCount} of ${total} BYE slot${total !== 1 ? 's' : ''} are eligible for late registration in FS Round 1.\n\n`;
if (eligibleCount === 0) {
message += '⛔ Late Registration is not possible — all BYE slots are blocked.\n\n';
if (ineligible.length > 0) {
message += 'Ineligible slots:\n';
ineligible.forEach(({ matchId, blocking }) => {
message += `• ${matchId} — blocked by: ${blocking.join(', ')}\n`;
});
}
showCommandFeedback('Late Registration', 'error', message);
return;
}
if (ineligible.length > 0) {
message += '⚠️ Draw fairness warning: not all BYE slots are available. The new player will be placed randomly among the eligible slots only.\n\n';
message += 'Ineligible slots:\n';
ineligible.forEach(({ matchId, blocking }) => {
message += `• ${matchId} — blocked by: ${blocking.join(', ')}\n`;
});
message += '\nBlocked matches must be stopped (if live) or undone (if completed) to become eligible. All affected matches will need to be replayed from the beginning.\n';
} else {
message += '✓ All BYE slots are eligible. The new player will be placed randomly.\n\n';
}
const status = ineligible.length > 0 ? 'warning' : 'success';
const statusColor = status === 'success' ? '#166534' : '#92400e';
const bgColor = status === 'success' ? '#f0fdf4' : '#fffbeb';
const statusIcon = status === 'success' ? '✓' : '⚠️';
const statusLabel = status === 'success' ? 'Success' : 'Warning';
const detailsHtml = message.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.map(line => line.startsWith('•')
? `
${line}
`
: `
${line}
`)
.join('');
const html = `
Command Executed: Late Registration
${statusIcon} ${statusLabel}
${detailsHtml}
Late Registration adds a new player to the tournament. The player must be entering the full tournament from the start — they will be placed randomly in an available walkover spot in Frontside Round 1, replacing a BYE.
Register New Player
`;
currentView = 'command-feedback';
updateRightPane(html);
setTimeout(() => document.getElementById('lateRegPlayerName')?.focus(), 100);
}
/**
* Late Registration Step 2: validate player name, pick random eligible slot, show confirmation gate.
*/
function commandLateRegistrationStep2() {
const nameInput = document.getElementById('lateRegPlayerName');
if (!nameInput) {
showCommandFeedback('Late Registration', 'error', 'Session expired. Please run Late Registration again.');
return;
}
const playerName = nameInput.value.trim();
if (!playerName) {
nameInput.style.borderColor = '#dc2626';
nameInput.focus();
return;
}
// Duplicate check
if (players.find(p => p.name.toLowerCase() === playerName.toLowerCase())) {
currentView = 'command-feedback';
updateRightPane(`
Command Executed: Late Registration
⚠️ Error
A player named ${playerName} is already registered in this tournament.
← Try again
`);
return;
}
// Re-run eligibility (state may have changed since Step 1)
const result = runLateRegistrationEligibility();
if (!result || result.eligible.length === 0) {
showCommandFeedback('Late Registration', 'error',
'No eligible BYE slots remain. The tournament state may have changed. Please run Late Registration again.');
return;
}
// Randomly select a slot — operator never chooses
const { eligible } = result;
const slotMatchId = eligible[Math.floor(Math.random() * eligible.length)];
const slotMatch = matches.find(m => m.id === slotMatchId);
// Opponent is the non-BYE player in the selected slot
const opponent = slotMatch.player1?.isBye ? slotMatch.player2 : slotMatch.player1;
const opponentName = opponent?.name || 'TBD';
// Store state for Step 3
lateRegState = { playerName, slotMatchId };
const html = `
Command Executed: Late Registration
⚠️ Confirm Placement
${playerName} will be placed in ${slotMatchId} vs ${opponentName}.
This operation cannot be undone.
Type ${playerName} to confirm:
← Cancel
`;
currentView = 'command-feedback';
updateRightPane(html);
setTimeout(() => document.getElementById('lateRegConfirmName')?.focus(), 100);
}
/**
* Late Registration Step 3: execute — register player, undo BYE auto-advance, place in bracket.
*/
function commandLateRegistrationStep3() {
if (!lateRegState) {
showCommandFeedback('Late Registration', 'error', 'Session expired. Please run Late Registration again.');
return;
}
const { playerName, slotMatchId } = lateRegState;
const confirmInput = document.getElementById('lateRegConfirmName');
// Exact-match confirmation gate
if (!confirmInput || confirmInput.value !== playerName) {
if (confirmInput) {
confirmInput.style.borderColor = '#dc2626';
confirmInput.focus();
}
return;
}
lateRegState = null;
// 1. Create and register the player (reusing addPlayer logic, bypassing the in-progress guard)
// the late player comes from the player database (found by name, or created)
const entry = typeof PlayerRegistry !== 'undefined' ? PlayerRegistry.findOrCreate(playerName) : null;
const newPlayer = {
id: Date.now(),
name: entry ? entry.short : playerName,
registryId: entry ? entry.id : undefined,
paid: true,
stats: { shortLegs: 0, highOuts: [], tons: 0, oneEighties: 0 },
placement: null,
eliminated: false
};
players.push(newPlayer);
if (typeof updatePlayersDisplay === 'function') updatePlayersDisplay();
if (typeof updatePlayerCount === 'function') updatePlayerCount();
// 2. Find the AUTO transaction for the BYE auto-advance
const history = getTournamentHistory();
const autoTransaction = history.find(t =>
t.matchId === slotMatchId &&
t.type === 'COMPLETE_MATCH' &&
t.completionType === 'AUTO'
);
if (!autoTransaction) {
showCommandFeedback('Late Registration', 'error',
`Could not find the auto-advance transaction for ${slotMatchId}.\n\nPlayer was registered but not placed in the bracket. Please contact support.`);
return;
}
// 3. Undo the BYE auto-advance using the surgical undo machinery
undoManualTransaction(autoTransaction.id);
// 4. Replace the BYE slot with the new player
const slotMatch = matches.find(m => m.id === slotMatchId);
if (slotMatch) {
if (slotMatch.player1?.isBye) {
slotMatch.player1 = { name: playerName, id: newPlayer.id };
} else {
slotMatch.player2 = { name: playerName, id: newPlayer.id };
}
}
// 5. Persist the updated match data before recording the transaction
if (typeof saveTournament === 'function') {
saveTournament();
}
// 6. Record LATE_REGISTRATION transaction in the audit log
saveTransaction({
id: `tx_${Date.now()}`,
type: 'LATE_REGISTRATION',
completionType: 'MANUAL',
description: `Late Registration: ${playerName} placed in ${slotMatchId}`,
timestamp: new Date().toISOString(),
matchId: slotMatchId,
playerName: playerName,
playerId: newPlayer.id
});
// 7. Refresh UI with the updated bracket
if (typeof refreshTournamentUI === 'function') {
refreshTournamentUI();
}
// 7. Show success
showCommandFeedback('Late Registration', 'success',
`${playerName} has been registered and placed in ${slotMatchId}.\n\nThe bracket has been updated. The match is now ready to play.`);
}
// ============================================================================
// TRANSACTION LOG MANAGEMENT
// ============================================================================
/**
* Show Transaction Log Management view
*/
function showTransactionLogManagement() {
currentView = 'transaction-management';
const history = getTournamentHistory ? getTournamentHistory() : [];
const stats = getTransactionStats();
const matchStats = getMatchStateStats();
const completedMatches = matches?.filter(m => m.completed) || [];
const activeMatches = matchStats.live + matchStats.ready;
// Calculate storage (rough estimate: ~2 bytes per char in UTF-16)
const storageEstimate = ((JSON.stringify(history).length * 2) / 1024 / 1024).toFixed(2);
const percentage = Math.round((stats.total / 1000) * 100);
// Estimate smart pruning savings
const estimate = estimateSmartPruning();
const estimatedSavings = estimate.removable;
const estimatedPercentage = stats.total > 0 ? Math.round((estimatedSavings / stats.total) * 100) : 0;
const estimatedStorageSaved = ((JSON.stringify(history).length * estimatedPercentage / 100 * 2) / 1024 / 1024).toFixed(2);
const html = `
Transaction Log Management
Current Status:
• Total transactions: ${stats.total}/1000 (${percentage}%)
• Storage used: ~${storageEstimate} MB
• Completed matches: ${completedMatches.length}
• Active matches: ${activeMatches} (${matchStats.live} live, ${matchStats.ready} ready)
Smart Pruning
Removes redundant transactions for completed matches:
${Object.entries(stats.breakdown).map(([type, count]) => {
const typePercentage = stats.total > 0 ? Math.round((count / stats.total) * 100) : 0;
let note = '';
if (type === 'COMPLETE_MATCH') note = ' - Never pruned';
else if (type === 'ASSIGN_LANE' || type === 'ASSIGN_REFEREE') note = ' - Keep only last selection';
else if (type === 'START_MATCH' || type === 'STOP_MATCH') note = ' - Redundant after completion';
return `
• ${type}: ${count} (${typePercentage}%)${note}
`;
}).join('')}
ℹ️ Note: Pruning removes redundant transaction history for completed matches. This is safe - the undo system uses separate state snapshots and will continue to work normally.
`;
updateRightPane(html);
}
/**
* Estimate smart pruning savings
*/
function estimateSmartPruning() {
const history = getTournamentHistory ? getTournamentHistory() : [];
const completedMatches = matches?.filter(m => m.completed) || [];
let removableCount = 0;
completedMatches.forEach(match => {
const matchId = match.id;
// Match strictly on matchId — a description substring check would let
// 'FS-1-1' claim transactions belonging to FS-1-10..FS-1-16
const matchTxns = history.filter(t => t.matchId === matchId);
// Count removable transactions
const lanes = matchTxns.filter(t => t.type === 'ASSIGN_LANE');
const refs = matchTxns.filter(t => t.type === 'ASSIGN_REFEREE');
const startStop = matchTxns.filter(t => t.type === 'START_MATCH' || t.type === 'STOP_MATCH');
// Remove all but last lane assignment
if (lanes.length > 1) {
removableCount += lanes.length - 1;
}
// Remove all but last referee assignment
if (refs.length > 1) {
removableCount += refs.length - 1;
}
// Remove all start/stop
removableCount += startStop.length;
});
return {
removable: removableCount,
completedMatches: completedMatches.length
};
}
/**
* Preview smart pruning - show what will be removed
*/
function previewSmartPruning() {
currentView = 'pruning-preview';
const history = getTournamentHistory ? getTournamentHistory() : [];
const completedMatches = matches?.filter(m => m.completed) || [];
const toRemove = [];
const analysis = {
lanes: { total: 0, keep: 0 },
refs: { total: 0, keep: 0 },
starts: 0,
stops: 0
};
completedMatches.forEach(match => {
const matchId = match.id;
// Match strictly on matchId — a description substring check would let
// 'FS-1-1' claim transactions belonging to FS-1-10..FS-1-16
const matchTxns = history.filter(t => t.matchId === matchId);
const lanes = matchTxns.filter(t => t.type === 'ASSIGN_LANE');
const refs = matchTxns.filter(t => t.type === 'ASSIGN_REFEREE');
const starts = matchTxns.filter(t => t.type === 'START_MATCH');
const stops = matchTxns.filter(t => t.type === 'STOP_MATCH');
analysis.lanes.total += lanes.length;
analysis.refs.total += refs.length;
analysis.starts += starts.length;
analysis.stops += stops.length;
// Keep only last lane assignment
if (lanes.length > 0) {
analysis.lanes.keep++;
toRemove.push(...lanes.slice(0, -1));
}
// Keep only last referee assignment
if (refs.length > 0) {
analysis.refs.keep++;
toRemove.push(...refs.slice(0, -1));
}
// Remove all start/stop
toRemove.push(...starts, ...stops);
});
const totalRemovable = toRemove.length;
const totalAfter = history.length - totalRemovable;
const reductionPercentage = history.length > 0 ? Math.round((totalRemovable / history.length) * 100) : 0;
const storageBefore = ((JSON.stringify(history).length * 2) / 1024 / 1024).toFixed(2);
const storageAfter = ((JSON.stringify(history).length * (100 - reductionPercentage) / 100 * 2) / 1024 / 1024).toFixed(2);
const storageSaved = (storageBefore - storageAfter).toFixed(2);
const html = `
Smart Pruning Preview
ℹ️ Note: Pruning removes redundant transaction history for completed matches.
This is safe - the undo system uses separate state snapshots and will continue to work normally.
Analysis complete for ${completedMatches.length} completed matches
Transactions to Remove:
• ${analysis.lanes.total - analysis.lanes.keep} old lane assignments (${analysis.lanes.total} total → ${analysis.lanes.keep} remain)
• ${analysis.refs.total - analysis.refs.keep} old referee assignments (${analysis.refs.total} total → ${analysis.refs.keep} remain)
• ${analysis.starts} match start events (all redundant)
• ${analysis.stops} match stop events (all redundant)
Total to remove: ${totalRemovable} transactions (${reductionPercentage}% reduction)
Transactions to Keep:
• ${completedMatches.length} match completions (all preserved)
• ${analysis.lanes.keep} final lane assignments (one per completed match)
• ${analysis.refs.keep} final referee assignments (one per completed match)