Add request locking, extended timeouts, and inactive layouts accordion
- Add request locking system to prevent concurrent scrapes of same course - Extend HTTP timeouts (10-30 min) for long-running scraping operations - Add comprehensive logging for layout parsing to debug silent failures - Implement accordion UI to hide layouts not played within 365 days - Return 409 status when scrape already in progress for a course - Add visual indicators for active vs inactive layouts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+153
-15
@@ -159,6 +159,61 @@
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.inactive-layouts-accordion {
|
||||
margin-top: 15px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.accordion-header:hover {
|
||||
background-color: #dee2e6;
|
||||
}
|
||||
|
||||
.accordion-header-text {
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.accordion-icon {
|
||||
transition: transform 0.3s;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.accordion-icon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-out;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.accordion-content.expanded {
|
||||
max-height: 2000px;
|
||||
padding: 10px;
|
||||
transition: max-height 0.5s ease-in;
|
||||
}
|
||||
|
||||
.layout-item.inactive {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.refresh-icon {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
@@ -288,6 +343,19 @@
|
||||
tableDiv.innerHTML = tableHTML;
|
||||
}
|
||||
|
||||
function toggleAccordion(accordionId) {
|
||||
const content = document.getElementById(accordionId);
|
||||
const icon = document.getElementById(`${accordionId}-icon`);
|
||||
|
||||
if (content.classList.contains('expanded')) {
|
||||
content.classList.remove('expanded');
|
||||
icon.classList.remove('expanded');
|
||||
} else {
|
||||
content.classList.add('expanded');
|
||||
icon.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleCourseLayouts(courseId) {
|
||||
const layoutsRow = document.getElementById(`layouts-${courseId}`);
|
||||
const layoutsContainer = document.getElementById(`layouts-container-${courseId}`);
|
||||
@@ -312,24 +380,91 @@
|
||||
const layouts = await response.json();
|
||||
|
||||
if (layouts.length > 0) {
|
||||
let layoutsHTML = '<h4 style="margin-top: 0;">Layouts:</h4>';
|
||||
// Calculate date threshold (365 days ago)
|
||||
const oneYearAgo = new Date();
|
||||
oneYearAgo.setDate(oneYearAgo.getDate() - 365);
|
||||
|
||||
// Separate layouts into active and inactive
|
||||
const activeLayouts = [];
|
||||
const inactiveLayouts = [];
|
||||
|
||||
layouts.forEach(layout => {
|
||||
const ratingDisplay = layout.mean_rating ?
|
||||
`<span style="color: #28a745; font-weight: bold; margin-left: 10px;">Rating: ${layout.mean_rating}</span>` :
|
||||
'';
|
||||
const dateDisplay = layout.last_played ?
|
||||
`<span style="color: #6c757d; font-size: 12px; margin-left: 10px;">Last played: ${new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}</span>` :
|
||||
'';
|
||||
layoutsHTML += `
|
||||
<div class="layout-item">
|
||||
<div>
|
||||
<span class="layout-name">${layout.name}</span>
|
||||
${dateDisplay}
|
||||
if (layout.last_played) {
|
||||
const lastPlayedDate = new Date(layout.last_played);
|
||||
if (lastPlayedDate >= oneYearAgo) {
|
||||
activeLayouts.push(layout);
|
||||
} else {
|
||||
inactiveLayouts.push(layout);
|
||||
}
|
||||
} else {
|
||||
// Never played -> inactive
|
||||
inactiveLayouts.push(layout);
|
||||
}
|
||||
});
|
||||
|
||||
let layoutsHTML = '<h4 style="margin-top: 0;">Layouts:</h4>';
|
||||
|
||||
// Render active layouts
|
||||
if (activeLayouts.length > 0) {
|
||||
activeLayouts.forEach(layout => {
|
||||
const ratingDisplay = layout.mean_rating ?
|
||||
`<span style="color: #28a745; font-weight: bold; margin-left: 10px;">Rating: ${layout.mean_rating}</span>` :
|
||||
'';
|
||||
const dateDisplay = layout.last_played ?
|
||||
`<span style="color: #6c757d; font-size: 12px; margin-left: 10px;">Last played: ${new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}</span>` :
|
||||
'';
|
||||
layoutsHTML += `
|
||||
<div class="layout-item">
|
||||
<div>
|
||||
<span class="layout-name">${layout.name}</span>
|
||||
${dateDisplay}
|
||||
</div>
|
||||
<span class="layout-par">Par ${layout.par}${ratingDisplay}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// Render inactive layouts in accordion
|
||||
if (inactiveLayouts.length > 0) {
|
||||
const accordionId = `accordion-${courseId}`;
|
||||
layoutsHTML += `
|
||||
<div class="inactive-layouts-accordion">
|
||||
<div class="accordion-header" onclick="toggleAccordion('${accordionId}')">
|
||||
<span class="accordion-header-text">Inactive Layouts (${inactiveLayouts.length}) - Not played in last year</span>
|
||||
<span class="accordion-icon" id="${accordionId}-icon">▼</span>
|
||||
</div>
|
||||
<div class="accordion-content" id="${accordionId}">
|
||||
`;
|
||||
|
||||
inactiveLayouts.forEach(layout => {
|
||||
const ratingDisplay = layout.mean_rating ?
|
||||
`<span style="color: #28a745; font-weight: bold; margin-left: 10px;">Rating: ${layout.mean_rating}</span>` :
|
||||
'';
|
||||
const dateDisplay = layout.last_played ?
|
||||
`<span style="color: #6c757d; font-size: 12px; margin-left: 10px;">Last played: ${new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}</span>` :
|
||||
`<span style="color: #dc3545; font-size: 12px; margin-left: 10px;">Never played</span>`;
|
||||
layoutsHTML += `
|
||||
<div class="layout-item inactive">
|
||||
<div>
|
||||
<span class="layout-name">${layout.name}</span>
|
||||
${dateDisplay}
|
||||
</div>
|
||||
<span class="layout-par">Par ${layout.par}${ratingDisplay}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
layoutsHTML += `
|
||||
</div>
|
||||
<span class="layout-par">Par ${layout.par}${ratingDisplay}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
if (activeLayouts.length === 0 && inactiveLayouts.length === 0) {
|
||||
layoutsHTML = '<div class="no-layouts">No layouts found. Click the refresh icon to scrape layouts.</div>';
|
||||
}
|
||||
|
||||
layoutsContainer.innerHTML = layoutsHTML;
|
||||
layoutsContainer.dataset.loaded = 'true';
|
||||
} else {
|
||||
@@ -379,7 +514,10 @@
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
if (response.status === 409) {
|
||||
// Scrape already in progress
|
||||
alert(data.message || 'Scrape already in progress for this course. Please wait.');
|
||||
} else if (data.success) {
|
||||
// Reset the loaded state to force reload
|
||||
const layoutsContainer = document.getElementById(`layouts-container-${courseId}`);
|
||||
layoutsContainer.dataset.loaded = 'false';
|
||||
|
||||
Reference in New Issue
Block a user