Add library git change detection and management system

Features:
- Yellow banner alert on Library tab when changes detected
- Custom diff viewer showing changed symbols/footprints by name
- Categorizes changes: symbols, footprints, and other files
- Color-coded status indicators (added/modified/deleted/untracked)
- Commit and push functionality from UI
- Smart authentication error handling with helpful messages
- Git configuration info display (remote URL, auth type, credential helper)
- Terminal opener for manual git credential setup
- Auto-refresh after commits to update status

Backend:
- check_library_git_status(): Checks for uncommitted/unpushed changes
- get_library_changed_parts(): Parses git status and categorizes by part type
- Socket handlers for git status, changes, commit, and push operations
- Authentication type detection (SSH vs HTTPS)
- Context-specific error messages for auth failures

Frontend:
- Git status banner with view changes button
- Modal with tabular display of changed parts
- Commit message input with commit/push buttons
- Real-time status updates during git operations
- Git config info section with terminal helper button

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
brentperteet
2026-02-28 12:14:03 -06:00
parent 7a7a9dfdcc
commit 4281fa2b97
2 changed files with 784 additions and 0 deletions

View File

@@ -402,6 +402,19 @@
<div class="container">
<h2>Parts Library Browser</h2>
<!-- Git Status Banner -->
<div id="libraryGitBanner" style="display: none; margin: 10px 0; padding: 15px; background-color: #fff3cd; border-left: 4px solid #ffc107; border-radius: 5px;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<strong style="color: #856404;">⚠ Library Repository has changes</strong>
<p id="gitBannerMessage" style="margin: 5px 0 0 0; color: #856404; font-size: 14px;"></p>
</div>
<div>
<button id="viewGitDiffBtn" class="btn" style="background-color: #ffc107; color: #000; padding: 8px 16px; font-size: 14px;">View Changes</button>
</div>
</div>
</div>
<!-- Browser View -->
<div id="libraryBrowserView">
<div style="margin: 20px 0;">
@@ -569,6 +582,41 @@
</div>
{% endif %}
<!-- Library Changes Modal -->
<div id="libraryChangesModal" class="modal">
<div class="modal-content" style="width: 70%; max-width: 900px;">
<span class="close" onclick="closeLibraryChangesModal()">&times;</span>
<h2>Library Changes</h2>
<!-- Git Config Info -->
<div id="gitConfigInfo" style="display: none; margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 5px; font-size: 13px;">
<strong>Repository:</strong> <span id="gitRemoteUrl" style="font-family: monospace;"></span><br>
<strong>Auth Type:</strong> <span id="gitAuthType"></span><br>
<strong>Credential Helper:</strong> <span id="gitCredHelper"></span>
<button id="openTerminalBtn" class="btn" style="margin-top: 5px; padding: 5px 10px; font-size: 12px; background-color: #6c757d;">Open Terminal for Setup</button>
</div>
<p>The following parts have been modified in the library repository:</p>
<div id="libraryChangesContent" style="max-height: 500px; overflow-y: auto;">
<div id="libraryChangesLoading" style="text-align: center; padding: 20px;">
<p>Loading changes...</p>
</div>
</div>
<div style="margin-top: 20px; padding-top: 20px; border-top: 2px solid #dee2e6;">
<div style="display: flex; gap: 10px; align-items: center;">
<input type="text" id="commitMessage" placeholder="Enter commit message..."
style="flex: 1; padding: 10px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
<button id="commitChangesBtn" class="btn" style="background-color: #28a745;">Commit Changes</button>
<button id="commitPushBtn" class="btn" style="background-color: #007bff;">Commit & Push</button>
<button class="btn secondary" onclick="closeLibraryChangesModal()">Close</button>
</div>
<div id="gitActionMessage" class="message" style="margin-top: 10px;"></div>
</div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
@@ -978,6 +1026,8 @@
} else if (tabName === 'library') {
document.getElementById('libraryTab').classList.add('active');
document.querySelectorAll('.tab')[2].classList.add('active');
// Check git status when library tab is shown
checkLibraryGitStatus();
} else if (tabName === 'variants') {
document.getElementById('variantsTab').classList.add('active');
document.querySelectorAll('.tab')[3].classList.add('active');
@@ -985,6 +1035,269 @@
}
}
// ========================================
// Library Git Status
// ========================================
let gitStatusData = null;
function checkLibraryGitStatus() {
socket.emit('check_library_git_status');
}
socket.on('library_git_status', (data) => {
console.log('[DEBUG] Received library_git_status:', data);
gitStatusData = data;
const banner = document.getElementById('libraryGitBanner');
const messageEl = document.getElementById('gitBannerMessage');
if (data.error) {
// Don't show banner if there's an error (e.g., not a git repo)
console.log('[DEBUG] Git status error:', data.error);
banner.style.display = 'none';
return;
}
console.log('[DEBUG] has_changes:', data.has_changes, 'has_unpushed:', data.has_unpushed);
if (data.has_changes || data.has_unpushed) {
let message = '';
if (data.has_changes && data.has_unpushed) {
message = 'The library repository has uncommitted changes and unpushed commits.';
} else if (data.has_changes) {
message = 'The library repository has uncommitted changes.';
} else if (data.has_unpushed) {
message = 'The library repository has unpushed commits.';
}
message += ` Branch: ${data.current_branch}`;
messageEl.textContent = message;
banner.style.display = 'block';
console.log('[DEBUG] Banner shown with message:', message);
} else {
banner.style.display = 'none';
console.log('[DEBUG] No changes detected, banner hidden');
}
});
document.getElementById('viewGitDiffBtn').addEventListener('click', () => {
showLibraryChangesModal();
});
function showLibraryChangesModal() {
const modal = document.getElementById('libraryChangesModal');
const content = document.getElementById('libraryChangesContent');
const loading = document.getElementById('libraryChangesLoading');
modal.style.display = 'block';
content.innerHTML = '';
loading.style.display = 'block';
content.appendChild(loading);
// Request git remote info and changes from server
socket.emit('get_git_remote_info');
socket.emit('get_library_changes');
}
socket.on('git_remote_info', (data) => {
const gitConfigInfo = document.getElementById('gitConfigInfo');
const gitRemoteUrl = document.getElementById('gitRemoteUrl');
const gitAuthType = document.getElementById('gitAuthType');
const gitCredHelper = document.getElementById('gitCredHelper');
if (data.error) {
gitConfigInfo.style.display = 'none';
return;
}
gitRemoteUrl.textContent = data.remote_url;
gitAuthType.textContent = data.uses_ssh ? 'SSH' : 'HTTPS';
gitCredHelper.textContent = data.credential_helper || 'Not configured';
gitConfigInfo.style.display = 'block';
});
document.getElementById('openTerminalBtn').addEventListener('click', () => {
socket.emit('setup_git_credentials');
});
socket.on('git_setup_complete', (data) => {
showGitActionMessage(data.message, 'success');
});
socket.on('git_setup_error', (data) => {
showGitActionMessage('Error: ' + data.error, 'error');
});
window.closeLibraryChangesModal = function() {
document.getElementById('libraryChangesModal').style.display = 'none';
document.getElementById('commitMessage').value = '';
document.getElementById('gitActionMessage').style.display = 'none';
}
socket.on('library_changes_result', (data) => {
const content = document.getElementById('libraryChangesContent');
if (data.error) {
content.innerHTML = `<p style="color: #dc3545;">Error: ${data.error}</p>`;
return;
}
if (data.total_changes === 0) {
content.innerHTML = '<p style="color: #6c757d;">No changes detected in the library.</p>';
return;
}
let html = '';
// Changed Symbols
if (data.symbols && data.symbols.length > 0) {
html += '<div style="margin-bottom: 20px;">';
html += '<h3 style="color: #007bff; border-bottom: 2px solid #007bff; padding-bottom: 5px;">Symbols</h3>';
html += '<table style="width: 100%; border-collapse: collapse; font-size: 14px;">';
html += '<thead><tr style="background: #f8f9fa;">';
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Library</th>';
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Status</th>';
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">File</th>';
html += '</tr></thead><tbody>';
for (const sym of data.symbols) {
const statusColor = getChangeTypeColor(sym.change_type);
html += '<tr>';
html += `<td style="padding: 8px; border: 1px solid #dee2e6;"><strong>${sym.library}</strong></td>`;
html += `<td style="padding: 8px; border: 1px solid #dee2e6;"><span style="color: ${statusColor}; font-weight: bold;">${sym.change_type.toUpperCase()}</span></td>`;
html += `<td style="padding: 8px; border: 1px solid #dee2e6; font-family: monospace; font-size: 12px;">${sym.file}</td>`;
html += '</tr>';
}
html += '</tbody></table></div>';
}
// Changed Footprints
if (data.footprints && data.footprints.length > 0) {
html += '<div style="margin-bottom: 20px;">';
html += '<h3 style="color: #28a745; border-bottom: 2px solid #28a745; padding-bottom: 5px;">Footprints</h3>';
html += '<table style="width: 100%; border-collapse: collapse; font-size: 14px;">';
html += '<thead><tr style="background: #f8f9fa;">';
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Library</th>';
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Footprint</th>';
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Status</th>';
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">File</th>';
html += '</tr></thead><tbody>';
for (const fp of data.footprints) {
const statusColor = getChangeTypeColor(fp.change_type);
html += '<tr>';
html += `<td style="padding: 8px; border: 1px solid #dee2e6;"><strong>${fp.library}</strong></td>`;
html += `<td style="padding: 8px; border: 1px solid #dee2e6;">${fp.footprint}</td>`;
html += `<td style="padding: 8px; border: 1px solid #dee2e6;"><span style="color: ${statusColor}; font-weight: bold;">${fp.change_type.toUpperCase()}</span></td>`;
html += `<td style="padding: 8px; border: 1px solid #dee2e6; font-family: monospace; font-size: 12px;">${fp.file}</td>`;
html += '</tr>';
}
html += '</tbody></table></div>';
}
// Other Changes
if (data.other && data.other.length > 0) {
html += '<div style="margin-bottom: 20px;">';
html += '<h3 style="color: #6c757d; border-bottom: 2px solid #6c757d; padding-bottom: 5px;">Other Files</h3>';
html += '<table style="width: 100%; border-collapse: collapse; font-size: 14px;">';
html += '<thead><tr style="background: #f8f9fa;">';
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">File</th>';
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Status</th>';
html += '</tr></thead><tbody>';
for (const other of data.other) {
const statusColor = getChangeTypeColor(other.change_type);
html += '<tr>';
html += `<td style="padding: 8px; border: 1px solid #dee2e6; font-family: monospace; font-size: 12px;">${other.file}</td>`;
html += `<td style="padding: 8px; border: 1px solid #dee2e6;"><span style="color: ${statusColor}; font-weight: bold;">${other.change_type.toUpperCase()}</span></td>`;
html += '</tr>';
}
html += '</tbody></table></div>';
}
content.innerHTML = html;
});
function getChangeTypeColor(changeType) {
switch(changeType) {
case 'added': return '#28a745';
case 'deleted': return '#dc3545';
case 'modified': return '#ffc107';
case 'untracked': return '#17a2b8';
default: return '#6c757d';
}
}
// Commit and Push Handlers
const gitActionMessage = document.getElementById('gitActionMessage');
const commitChangesBtn = document.getElementById('commitChangesBtn');
const commitPushBtn = document.getElementById('commitPushBtn');
const commitMessageInput = document.getElementById('commitMessage');
function showGitActionMessage(text, type) {
gitActionMessage.textContent = text;
gitActionMessage.className = 'message ' + type;
gitActionMessage.style.display = 'block';
}
commitChangesBtn.addEventListener('click', () => {
const message = commitMessageInput.value.trim();
if (!message) {
showGitActionMessage('Please enter a commit message', 'error');
return;
}
commitChangesBtn.disabled = true;
commitPushBtn.disabled = true;
showGitActionMessage('Committing changes...', 'info');
socket.emit('commit_library_changes', { message: message });
});
commitPushBtn.addEventListener('click', () => {
const message = commitMessageInput.value.trim();
if (!message) {
showGitActionMessage('Please enter a commit message', 'error');
return;
}
commitChangesBtn.disabled = true;
commitPushBtn.disabled = true;
showGitActionMessage('Committing and pushing changes...', 'info');
socket.emit('commit_push_library_changes', { message: message });
});
socket.on('library_commit_status', (data) => {
showGitActionMessage(data.status, 'info');
});
socket.on('library_commit_complete', (data) => {
showGitActionMessage(data.message, 'success');
commitChangesBtn.disabled = false;
commitPushBtn.disabled = false;
commitMessageInput.value = '';
// Refresh the changes list
setTimeout(() => {
socket.emit('get_library_changes');
}, 1000);
});
socket.on('library_commit_error', (data) => {
// Format error message with line breaks preserved
const errorMsg = data.error.replace(/\n/g, '<br>');
gitActionMessage.innerHTML = '<strong>Error:</strong><br>' + errorMsg;
gitActionMessage.className = 'message error';
gitActionMessage.style.display = 'block';
commitChangesBtn.disabled = false;
commitPushBtn.disabled = false;
});
// Check git status on initial load if in library-only mode
if (libraryOnlyMode) {
checkLibraryGitStatus();
}
// ========================================
// Library Browser
// ========================================