tractatus/public/js/admin/claude-md-migrator.js
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

523 lines
19 KiB
JavaScript

/**
* CLAUDE.md Migration Wizard
* Handles multi-step migration of CLAUDE.md rules to database
*/
let analysisResult = null;
let selectedCandidates = [];
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initializeEventListeners();
checkAuth();
});
/**
* Initialize all event listeners
*/
function initializeEventListeners() {
// Step 1: Upload
document.getElementById('file-upload').addEventListener('change', handleFileUpload);
document.getElementById('analyze-btn').addEventListener('click', analyzeClaudeMd);
// Step 2: Review
document.getElementById('back-to-upload-btn').addEventListener('click', () => goToStep(1));
document.getElementById('create-rules-btn').addEventListener('click', createSelectedRules);
// Step 3: Results
document.getElementById('migrate-another-btn').addEventListener('click', () => goToStep(1));
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => switchTab(e.target.dataset.tab));
});
// Logout
document.getElementById('logout-btn').addEventListener('click', logout);
}
/**
* Check authentication
*/
async function checkAuth() {
const token = localStorage.getItem('admin_token');
if (!token) {
window.location.href = '/admin/login.html';
}
}
/**
* API request helper
*/
async function apiRequest(endpoint, options = {}) {
const token = localStorage.getItem('admin_token');
const response = await fetch(endpoint, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
}
});
if (response.status === 401) {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
window.location.href = '/admin/login.html';
return;
}
return response.json();
}
/**
* Handle file upload
*/
function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
document.getElementById('claude-md-content').value = e.target.result;
showToast('File loaded successfully', 'success');
};
reader.onerror = () => {
showToast('Failed to read file', 'error');
};
reader.readAsText(file);
}
/**
* Analyze CLAUDE.md content
*/
async function analyzeClaudeMd() {
const content = document.getElementById('claude-md-content').value.trim();
if (!content) {
showToast('Please upload or paste CLAUDE.md content', 'error');
return;
}
const analyzeBtn = document.getElementById('analyze-btn');
analyzeBtn.disabled = true;
analyzeBtn.textContent = 'Analyzing...';
try {
const response = await apiRequest('/api/admin/rules/analyze-claude-md', {
method: 'POST',
body: JSON.stringify({ content })
});
if (!response.success) {
throw new Error(response.message || 'Analysis failed');
}
analysisResult = response.analysis;
displayAnalysisResults(analysisResult);
goToStep(2);
} catch (error) {
console.error('Analysis error:', error);
showToast(error.message || 'Failed to analyze CLAUDE.md', 'error');
} finally {
analyzeBtn.disabled = false;
analyzeBtn.textContent = 'Analyze CLAUDE.md';
}
}
/**
* Display analysis results
*/
function displayAnalysisResults(analysis) {
// Update statistics
document.getElementById('stat-total').textContent = analysis.totalStatements;
document.getElementById('stat-high-quality').textContent = analysis.quality.highQuality;
document.getElementById('stat-needs-clarification').textContent = analysis.quality.needsClarification;
document.getElementById('stat-too-nebulous').textContent = analysis.quality.tooNebulous;
// Reset selected candidates
selectedCandidates = [];
// Display high-quality candidates (auto-selected)
const highQualityList = document.getElementById('high-quality-list');
const highQualityCandidates = analysis.candidates.filter(c => c.quality === 'HIGH');
if (highQualityCandidates.length > 0) {
highQualityList.innerHTML = highQualityCandidates.map((candidate, index) => `
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<div class="flex items-start">
<input
type="checkbox"
id="candidate-high-${index}"
class="mt-1 h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
checked
data-change-action="toggleCandidate" data-index="${index}"
>
<div class="ml-3 flex-1">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-green-900">${escapeHtml(candidate.sectionTitle)}</span>
<div class="flex items-center space-x-2">
<span class="px-2 py-1 text-xs rounded-full ${getQuadrantColor(candidate.quadrant)}">${candidate.quadrant}</span>
<span class="px-2 py-1 text-xs rounded-full ${getPersistenceColor(candidate.persistence)}">${candidate.persistence}</span>
</div>
</div>
<div class="mt-2 space-y-2">
<div>
<p class="text-xs text-gray-600">Original:</p>
<p class="text-sm text-gray-800">${escapeHtml(candidate.originalText)}</p>
</div>
<div>
<p class="text-xs text-gray-600">Suggested:</p>
<p class="text-sm font-medium text-green-900">${escapeHtml(candidate.suggestedRule.text)}</p>
</div>
${candidate.suggestedRule.variables && candidate.suggestedRule.variables.length > 0 ? `
<div class="flex flex-wrap gap-1">
${candidate.suggestedRule.variables.map(v => `
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
\${${v}}
</span>
`).join('')}
</div>
` : ''}
<div class="flex items-center space-x-4 text-xs">
<span class="text-gray-600">Clarity: <span class="font-medium">${candidate.suggestedRule.clarityScore}%</span></span>
<span class="text-gray-600">Scope: <span class="font-medium">${candidate.suggestedRule.scope}</span></span>
</div>
</div>
</div>
</div>
</div>
`).join('');
// Auto-select high-quality candidates
highQualityCandidates.forEach(c => selectedCandidates.push(c));
} else {
highQualityList.innerHTML = '<p class="text-sm text-gray-500">No high-quality candidates found.</p>';
}
// Display needs clarification candidates
const needsClarificationList = document.getElementById('needs-clarification-list');
const needsClarificationCandidates = analysis.candidates.filter(c => c.quality === 'NEEDS_CLARIFICATION');
if (needsClarificationCandidates.length > 0) {
needsClarificationList.innerHTML = needsClarificationCandidates.map((candidate, index) => `
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div class="flex items-start">
<input
type="checkbox"
id="candidate-needs-${index}"
class="mt-1 h-4 w-4 text-yellow-600 focus:ring-yellow-500 border-gray-300 rounded"
data-change-action="toggleCandidate" data-index="${index}"
>
<div class="ml-3 flex-1">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-yellow-900">${escapeHtml(candidate.sectionTitle)}</span>
<div class="flex items-center space-x-2">
<span class="px-2 py-1 text-xs rounded-full ${getQuadrantColor(candidate.quadrant)}">${candidate.quadrant}</span>
<span class="px-2 py-1 text-xs rounded-full ${getPersistenceColor(candidate.persistence)}">${candidate.persistence}</span>
</div>
</div>
<div class="mt-2 space-y-2">
<div>
<p class="text-xs text-gray-600">Original:</p>
<p class="text-sm text-gray-800">${escapeHtml(candidate.originalText)}</p>
</div>
<div>
<p class="text-xs text-gray-600">Suggested:</p>
<p class="text-sm font-medium text-yellow-900">${escapeHtml(candidate.suggestedRule.text)}</p>
</div>
${candidate.analysis.issues && candidate.analysis.issues.length > 0 ? `
<div class="bg-yellow-100 rounded p-2">
<p class="text-xs font-medium text-yellow-900 mb-1">Issues:</p>
<ul class="list-disc list-inside text-xs text-yellow-800 space-y-1">
${candidate.analysis.issues.map(issue => `<li>${escapeHtml(issue)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
</div>
</div>
`).join('');
} else {
needsClarificationList.innerHTML = '<p class="text-sm text-gray-500">No candidates needing clarification.</p>';
}
// Display too nebulous candidates
const tooNebulousList = document.getElementById('too-nebulous-list');
const tooNebulousCandidates = analysis.candidates.filter(c => c.quality === 'TOO_NEBULOUS');
if (tooNebulousCandidates.length > 0) {
tooNebulousList.innerHTML = tooNebulousCandidates.map(candidate => `
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
<div class="flex items-start">
<svg class="h-5 w-5 text-red-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<div class="ml-3 flex-1">
<p class="text-xs font-medium text-red-900 mb-2">${escapeHtml(candidate.sectionTitle)}</p>
<p class="text-sm text-red-800 mb-2">${escapeHtml(candidate.originalText)}</p>
${candidate.improvements && candidate.improvements.length > 0 ? `
<div class="bg-red-100 rounded p-2">
<p class="text-xs font-medium text-red-900 mb-1">Suggestions:</p>
<ul class="list-disc list-inside text-xs text-red-800 space-y-1">
${candidate.improvements.map(imp => `<li>${escapeHtml(imp)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
</div>
`).join('');
} else {
tooNebulousList.innerHTML = '<p class="text-sm text-gray-500">No too-nebulous statements.</p>';
}
// Display redundancies
const redundanciesList = document.getElementById('redundancies-list');
if (analysis.redundancies && analysis.redundancies.length > 0) {
redundanciesList.innerHTML = analysis.redundancies.map((group, index) => `
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p class="text-xs font-medium text-blue-900 mb-2">Redundancy Group ${index + 1}</p>
<div class="space-y-2">
${group.rules.map(rule => `
<p class="text-sm text-gray-800">• ${escapeHtml(rule)}</p>
`).join('')}
</div>
<div class="mt-3 bg-blue-100 rounded p-2">
<p class="text-xs font-medium text-blue-900 mb-1">Suggested Merge:</p>
<p class="text-sm font-medium text-blue-800">${escapeHtml(group.mergeSuggestion)}</p>
</div>
</div>
`).join('');
} else {
redundanciesList.innerHTML = '<p class="text-sm text-gray-500">No redundancies detected.</p>';
}
}
/**
* Toggle candidate selection
*/
function toggleCandidate(candidate, checked) {
if (checked) {
selectedCandidates.push(candidate);
} else {
selectedCandidates = selectedCandidates.filter(c => c.originalText !== candidate.originalText);
}
// Update button text
document.getElementById('create-rules-btn').textContent =
`Create Selected Rules (${selectedCandidates.length})`;
}
/**
* Create selected rules
*/
async function createSelectedRules() {
if (selectedCandidates.length === 0) {
showToast('Please select at least one rule to create', 'error');
return;
}
const createBtn = document.getElementById('create-rules-btn');
createBtn.disabled = true;
createBtn.textContent = 'Creating...';
try {
const response = await apiRequest('/api/admin/rules/migrate-from-claude-md', {
method: 'POST',
body: JSON.stringify({ selectedCandidates })
});
if (!response.success) {
throw new Error(response.message || 'Migration failed');
}
displayMigrationResults(response.results);
goToStep(3);
} catch (error) {
console.error('Migration error:', error);
showToast(error.message || 'Failed to create rules', 'error');
createBtn.disabled = false;
createBtn.textContent = `Create Selected Rules (${selectedCandidates.length})`;
}
}
/**
* Display migration results
*/
function displayMigrationResults(results) {
const summaryDiv = document.getElementById('results-summary');
summaryDiv.innerHTML = `
<div class="bg-white rounded-lg p-6 shadow-sm border border-gray-200 max-w-md mx-auto">
<div class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-gray-700">Total Requested:</span>
<span class="font-semibold text-gray-900">${results.totalRequested}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-700">Successfully Created:</span>
<span class="font-semibold text-green-600">${results.created.length}</span>
</div>
${results.failed.length > 0 ? `
<div class="flex justify-between items-center">
<span class="text-gray-700">Failed:</span>
<span class="font-semibold text-red-600">${results.failed.length}</span>
</div>
` : ''}
</div>
${results.created.length > 0 ? `
<div class="mt-6">
<p class="text-sm font-medium text-gray-700 mb-2">Created Rules:</p>
<div class="space-y-2">
${results.created.map(rule => `
<div class="bg-gray-50 rounded p-2 text-xs">
<span class="font-mono font-medium text-indigo-600">${escapeHtml(rule.id)}</span>
<p class="text-gray-700 mt-1">${escapeHtml(rule.text.substring(0, 80))}${rule.text.length > 80 ? '...' : ''}</p>
</div>
`).join('')}
</div>
</div>
` : ''}
${results.failed.length > 0 ? `
<div class="mt-6">
<p class="text-sm font-medium text-red-700 mb-2">Failed Rules:</p>
<div class="space-y-2">
${results.failed.map(fail => `
<div class="bg-red-50 rounded p-2 text-xs">
<p class="text-red-700">${escapeHtml(fail.candidate.substring(0, 60))}...</p>
<p class="text-red-600 mt-1">Error: ${escapeHtml(fail.error)}</p>
</div>
`).join('')}
</div>
</div>
` : ''}
</div>
`;
}
/**
* Switch between tabs
*/
function switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
if (btn.dataset.tab === tabName) {
btn.classList.add('active', 'border-indigo-600', 'text-indigo-600');
btn.classList.remove('border-transparent', 'text-gray-500');
} else {
btn.classList.remove('active', 'border-indigo-600', 'text-indigo-600');
btn.classList.add('border-transparent', 'text-gray-500');
}
});
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(`${tabName}-tab`).classList.remove('hidden');
}
/**
* Navigate to a specific step
*/
function goToStep(stepNumber) {
// Hide all steps
[1, 2, 3].forEach(num => {
document.getElementById(`step-${num}-content`).classList.add('hidden');
});
// Show target step
document.getElementById(`step-${stepNumber}-content`).classList.remove('hidden');
// Update step indicators
[1, 2, 3].forEach(num => {
const indicator = document.getElementById(`step-${num}-indicator`);
const title = document.getElementById(`step-${num}-title`);
if (num < stepNumber) {
// Completed step
indicator.className = 'flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full bg-green-600 text-white font-semibold';
indicator.innerHTML = '<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
title.classList.add('text-gray-900');
title.classList.remove('text-gray-500');
} else if (num === stepNumber) {
// Current step
indicator.className = 'flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full bg-indigo-600 text-white font-semibold';
indicator.textContent = num;
title.classList.add('text-gray-900');
title.classList.remove('text-gray-500');
} else {
// Future step
indicator.className = 'flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full bg-gray-200 text-gray-500 font-semibold';
indicator.textContent = num;
title.classList.remove('text-gray-900');
title.classList.add('text-gray-500');
}
});
// Reset form if going back to step 1
if (stepNumber === 1) {
document.getElementById('claude-md-content').value = '';
document.getElementById('file-upload').value = '';
analysisResult = null;
selectedCandidates = [];
}
}
/**
* Logout
*/
function logout() {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
window.location.href = '/admin/login.html';
}
// Utility functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function getQuadrantColor(quadrant) {
const colors = {
STRATEGIC: 'bg-purple-100 text-purple-800',
OPERATIONAL: 'bg-green-100 text-green-800',
TACTICAL: 'bg-yellow-100 text-yellow-800',
SYSTEM: 'bg-blue-100 text-blue-800',
STORAGE: 'bg-gray-100 text-gray-800'
};
return colors[quadrant] || 'bg-gray-100 text-gray-800';
}
function getPersistenceColor(persistence) {
const colors = {
HIGH: 'bg-red-100 text-red-800',
MEDIUM: 'bg-orange-100 text-orange-800',
LOW: 'bg-yellow-100 text-yellow-800'
};
return colors[persistence] || 'bg-gray-100 text-gray-800';
}
// Event delegation for data-change-action checkboxes (CSP compliance)
document.addEventListener('change', (e) => {
const checkbox = e.target.closest('[data-change-action]');
if (!checkbox) return;
const action = checkbox.dataset.changeAction;
const index = parseInt(checkbox.dataset.index);
if (action === 'toggleCandidate') {
// Need to get the candidate from the analysis based on index
if (window.currentAnalysis && window.currentAnalysis.candidates[index]) {
toggleCandidate(window.currentAnalysis.candidates[index], checkbox.checked);
}
}
});