- Implement PostgreSQL database schema with users and bookmarks tables - Add database connection pooling with retry logic and error handling - Create migration system with automatic schema initialization - Add database CLI tools for management (init, status, validate, etc.) - Include comprehensive error handling and diagnostics - Add development seed data and testing utilities - Implement health monitoring and connection pool statistics - Create detailed documentation and troubleshooting guide Database features: - Users table with authentication fields and email verification - Bookmarks table with user association and metadata - Proper indexes for performance optimization - Automatic timestamp triggers - Transaction support with rollback handling - Connection pooling (20 max connections, 30s idle timeout) - Graceful shutdown handling CLI commands available: - npm run db:init - Initialize database - npm run db:status - Check database status - npm run db:validate - Validate schema - npm run db:test - Run database tests - npm run db:diagnostics - Full diagnostics
417 lines
17 KiB
HTML
417 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Bookmark Metadata Functionality Test</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background-color: #f5f5f5;
|
|
}
|
|
.test-container {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
margin-bottom: 20px;
|
|
}
|
|
.test-result {
|
|
padding: 10px;
|
|
margin: 5px 0;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
}
|
|
.success {
|
|
background-color: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
.error {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
.info {
|
|
background-color: #d1ecf1;
|
|
color: #0c5460;
|
|
border: 1px solid #bee5eb;
|
|
}
|
|
.test-section {
|
|
margin-bottom: 30px;
|
|
padding: 15px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
}
|
|
.test-section h3 {
|
|
margin-top: 0;
|
|
color: #2c3e50;
|
|
}
|
|
.bookmark-preview {
|
|
background: #f8f9fa;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin: 10px 0;
|
|
border-left: 4px solid #3498db;
|
|
}
|
|
.metadata-display {
|
|
margin-top: 10px;
|
|
font-size: 14px;
|
|
}
|
|
.tag {
|
|
background: #e3f2fd;
|
|
color: #1976d2;
|
|
padding: 2px 6px;
|
|
border-radius: 12px;
|
|
font-size: 11px;
|
|
margin-right: 4px;
|
|
}
|
|
.rating {
|
|
color: #ffc107;
|
|
font-size: 16px;
|
|
}
|
|
.favorite {
|
|
color: #e74c3c;
|
|
font-size: 16px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="test-container">
|
|
<h1>Bookmark Metadata Functionality Test</h1>
|
|
<p>This page tests the new bookmark metadata and tagging system functionality.</p>
|
|
|
|
<div id="testResults"></div>
|
|
|
|
<div class="test-section">
|
|
<h3>Test Bookmark with Metadata</h3>
|
|
<div id="bookmarkPreview" class="bookmark-preview">
|
|
<!-- Test bookmark will be displayed here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Create a test instance of BookmarkManager
|
|
class TestBookmarkManager {
|
|
constructor() {
|
|
this.bookmarks = [];
|
|
this.currentEditId = null;
|
|
this.currentFilter = 'all';
|
|
this.searchTimeout = null;
|
|
this.virtualScrollThreshold = 100;
|
|
this.itemsPerPage = 50;
|
|
this.currentPage = 1;
|
|
this.isLoading = false;
|
|
|
|
// Initialize with test data
|
|
this.initializeTestData();
|
|
}
|
|
|
|
initializeTestData() {
|
|
// Create test bookmarks with metadata
|
|
this.bookmarks = [
|
|
{
|
|
id: 1,
|
|
title: "JavaScript Tutorial",
|
|
url: "https://developer.mozilla.org/en-US/docs/Web/JavaScript",
|
|
folder: "Development",
|
|
tags: ["javascript", "tutorial", "web-development", "programming"],
|
|
notes: "Comprehensive JavaScript guide from MDN. Great for beginners and advanced developers.",
|
|
rating: 5,
|
|
favorite: true,
|
|
addDate: Date.now() - 86400000, // 1 day ago
|
|
lastModified: Date.now() - 3600000, // 1 hour ago
|
|
lastVisited: Date.now() - 1800000, // 30 minutes ago
|
|
icon: '',
|
|
status: 'valid'
|
|
},
|
|
{
|
|
id: 2,
|
|
title: "React Documentation",
|
|
url: "https://reactjs.org/docs/getting-started.html",
|
|
folder: "Development",
|
|
tags: ["react", "frontend", "library"],
|
|
notes: "Official React documentation with examples and best practices.",
|
|
rating: 4,
|
|
favorite: false,
|
|
addDate: Date.now() - 172800000, // 2 days ago
|
|
lastModified: Date.now() - 86400000, // 1 day ago
|
|
lastVisited: null,
|
|
icon: '',
|
|
status: 'valid'
|
|
},
|
|
{
|
|
id: 3,
|
|
title: "Favorite Recipe Blog",
|
|
url: "https://example-recipes.com",
|
|
folder: "Personal",
|
|
tags: ["cooking", "recipes", "food"],
|
|
notes: "Amazing collection of healthy recipes. Check the dessert section!",
|
|
rating: 3,
|
|
favorite: true,
|
|
addDate: Date.now() - 259200000, // 3 days ago
|
|
lastModified: Date.now() - 172800000, // 2 days ago
|
|
lastVisited: Date.now() - 7200000, // 2 hours ago
|
|
icon: '',
|
|
status: 'unknown'
|
|
}
|
|
];
|
|
}
|
|
|
|
// Helper function to format relative time
|
|
formatRelativeTime(timestamp) {
|
|
const now = Date.now();
|
|
const diff = now - timestamp;
|
|
|
|
const minutes = Math.floor(diff / (1000 * 60));
|
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
|
|
if (minutes < 60) {
|
|
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
|
} else if (hours < 24) {
|
|
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
|
} else {
|
|
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
|
}
|
|
}
|
|
|
|
// Test search functionality with metadata
|
|
searchBookmarks(query) {
|
|
const lowerQuery = query.toLowerCase();
|
|
return this.bookmarks.filter(bookmark => {
|
|
if (bookmark.title.toLowerCase().includes(lowerQuery)) return true;
|
|
if (bookmark.url.toLowerCase().includes(lowerQuery)) return true;
|
|
if (bookmark.folder && bookmark.folder.toLowerCase().includes(lowerQuery)) return true;
|
|
if (bookmark.tags && bookmark.tags.some(tag => tag.toLowerCase().includes(lowerQuery))) return true;
|
|
if (bookmark.notes && bookmark.notes.toLowerCase().includes(lowerQuery)) return true;
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// Test filtering by favorites
|
|
getFavoriteBookmarks() {
|
|
return this.bookmarks.filter(b => b.favorite);
|
|
}
|
|
|
|
// Test filtering by rating
|
|
getBookmarksByRating(minRating) {
|
|
return this.bookmarks.filter(b => b.rating >= minRating);
|
|
}
|
|
|
|
// Test export functionality with metadata
|
|
generateJSONExport(bookmarksToExport) {
|
|
const exportData = {
|
|
exportDate: new Date().toISOString(),
|
|
version: '1.1',
|
|
totalBookmarks: bookmarksToExport.length,
|
|
bookmarks: bookmarksToExport.map(bookmark => ({
|
|
id: bookmark.id,
|
|
title: bookmark.title,
|
|
url: bookmark.url,
|
|
folder: bookmark.folder || '',
|
|
tags: bookmark.tags || [],
|
|
notes: bookmark.notes || '',
|
|
rating: bookmark.rating || 0,
|
|
favorite: bookmark.favorite || false,
|
|
addDate: bookmark.addDate,
|
|
lastModified: bookmark.lastModified,
|
|
lastVisited: bookmark.lastVisited,
|
|
icon: bookmark.icon || '',
|
|
status: bookmark.status
|
|
}))
|
|
};
|
|
|
|
return JSON.stringify(exportData, null, 2);
|
|
}
|
|
}
|
|
|
|
// Run tests
|
|
function runTests() {
|
|
const testManager = new TestBookmarkManager();
|
|
let results = '<h2>Test Results</h2>';
|
|
|
|
// Test 1: Basic metadata storage
|
|
try {
|
|
const bookmark = testManager.bookmarks[0];
|
|
const hasAllMetadata = bookmark.tags && bookmark.notes &&
|
|
typeof bookmark.rating === 'number' &&
|
|
typeof bookmark.favorite === 'boolean';
|
|
|
|
results += `
|
|
<div class="test-result ${hasAllMetadata ? 'success' : 'error'}">
|
|
Metadata Storage Test: ${hasAllMetadata ? 'PASSED' : 'FAILED'}
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
results += `
|
|
<div class="test-result error">
|
|
Metadata Storage Test: FAILED - ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Test 2: Search functionality with tags
|
|
try {
|
|
const searchResults = testManager.searchBookmarks('javascript');
|
|
const foundByTag = searchResults.length > 0 &&
|
|
searchResults.some(b => b.tags.includes('javascript'));
|
|
|
|
results += `
|
|
<div class="test-result ${foundByTag ? 'success' : 'error'}">
|
|
Tag Search Test: ${foundByTag ? 'PASSED' : 'FAILED'} (Found ${searchResults.length} results)
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
results += `
|
|
<div class="test-result error">
|
|
Tag Search Test: FAILED - ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Test 3: Search functionality with notes
|
|
try {
|
|
const searchResults = testManager.searchBookmarks('healthy recipes');
|
|
const foundByNotes = searchResults.length > 0;
|
|
|
|
results += `
|
|
<div class="test-result ${foundByNotes ? 'success' : 'error'}">
|
|
Notes Search Test: ${foundByNotes ? 'PASSED' : 'FAILED'} (Found ${searchResults.length} results)
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
results += `
|
|
<div class="test-result error">
|
|
Notes Search Test: FAILED - ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Test 4: Favorite filtering
|
|
try {
|
|
const favorites = testManager.getFavoriteBookmarks();
|
|
const correctFavorites = favorites.length === 2; // Should find 2 favorites
|
|
|
|
results += `
|
|
<div class="test-result ${correctFavorites ? 'success' : 'error'}">
|
|
Favorite Filter Test: ${correctFavorites ? 'PASSED' : 'FAILED'} (Found ${favorites.length} favorites)
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
results += `
|
|
<div class="test-result error">
|
|
Favorite Filter Test: FAILED - ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Test 5: Rating filtering
|
|
try {
|
|
const highRated = testManager.getBookmarksByRating(4);
|
|
const correctRating = highRated.length === 2; // Should find 2 bookmarks with rating >= 4
|
|
|
|
results += `
|
|
<div class="test-result ${correctRating ? 'success' : 'error'}">
|
|
Rating Filter Test: ${correctRating ? 'PASSED' : 'FAILED'} (Found ${highRated.length} high-rated bookmarks)
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
results += `
|
|
<div class="test-result error">
|
|
Rating Filter Test: FAILED - ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Test 6: JSON export with metadata
|
|
try {
|
|
const jsonExport = testManager.generateJSONExport(testManager.bookmarks);
|
|
const parsed = JSON.parse(jsonExport);
|
|
const hasMetadataInExport = parsed.bookmarks[0].tags &&
|
|
parsed.bookmarks[0].notes &&
|
|
typeof parsed.bookmarks[0].rating === 'number' &&
|
|
typeof parsed.bookmarks[0].favorite === 'boolean';
|
|
|
|
results += `
|
|
<div class="test-result ${hasMetadataInExport ? 'success' : 'error'}">
|
|
JSON Export Test: ${hasMetadataInExport ? 'PASSED' : 'FAILED'}
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
results += `
|
|
<div class="test-result error">
|
|
JSON Export Test: FAILED - ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Test 7: Last visited tracking
|
|
try {
|
|
const bookmark = testManager.bookmarks[0];
|
|
const hasLastVisited = bookmark.lastVisited && typeof bookmark.lastVisited === 'number';
|
|
|
|
results += `
|
|
<div class="test-result ${hasLastVisited ? 'success' : 'error'}">
|
|
Last Visited Tracking Test: ${hasLastVisited ? 'PASSED' : 'FAILED'}
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
results += `
|
|
<div class="test-result error">
|
|
Last Visited Tracking Test: FAILED - ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
document.getElementById('testResults').innerHTML = results;
|
|
|
|
// Display test bookmark with metadata
|
|
displayTestBookmark(testManager.bookmarks[0], testManager);
|
|
}
|
|
|
|
function displayTestBookmark(bookmark, manager) {
|
|
const preview = document.getElementById('bookmarkPreview');
|
|
|
|
let html = `
|
|
<h4>${bookmark.title}</h4>
|
|
<div style="color: #666; margin-bottom: 10px;">${bookmark.url}</div>
|
|
<div style="color: #3498db; font-size: 12px; margin-bottom: 10px;">Folder: ${bookmark.folder}</div>
|
|
|
|
<div class="metadata-display">
|
|
<div style="margin-bottom: 8px;">
|
|
<strong>Tags:</strong>
|
|
${bookmark.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
|
|
</div>
|
|
|
|
<div style="margin-bottom: 8px;">
|
|
<strong>Rating:</strong>
|
|
<span class="rating">${'★'.repeat(bookmark.rating)}${'☆'.repeat(5 - bookmark.rating)}</span>
|
|
${bookmark.favorite ? '<span class="favorite">❤️ Favorite</span>' : ''}
|
|
</div>
|
|
|
|
<div style="margin-bottom: 8px;">
|
|
<strong>Notes:</strong>
|
|
<div style="font-style: italic; color: #666; margin-top: 4px;">${bookmark.notes}</div>
|
|
</div>
|
|
|
|
<div style="font-size: 12px; color: #999;">
|
|
<div>Added: ${manager.formatRelativeTime(bookmark.addDate)}</div>
|
|
<div>Last modified: ${manager.formatRelativeTime(bookmark.lastModified)}</div>
|
|
<div>Last visited: ${manager.formatRelativeTime(bookmark.lastVisited)}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
preview.innerHTML = html;
|
|
}
|
|
|
|
// Run tests when page loads
|
|
document.addEventListener('DOMContentLoaded', runTests);
|
|
</script>
|
|
</body>
|
|
</html> |