feat: Add comprehensive database status web interface

- Create /database page showing database statistics, size, and location
- Display storage efficiency, page statistics, and optimization info
- Show external data sources with license information and load status
- Include auto-refresh functionality and navigation links
- Implement CSS overrides to fix scrolling issues

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-31 19:40:12 +02:00
commit ce7710d1b4

360
assets/static/database.html Normal file
View file

@ -0,0 +1,360 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Database Status - SkyView</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" href="/static/css/style.css">
<style>
.database-container {
max-width: 1000px;
margin: 20px auto;
padding: 20px;
}
.status-card {
background: var(--card-bg, #fff);
border: 1px solid var(--border-color, #ddd);
border-radius: 8px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.status-card h2 {
margin-top: 0;
color: var(--text-primary, #333);
border-bottom: 2px solid var(--accent-color, #007acc);
padding-bottom: 10px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 15px 0;
}
.stat-item {
background: var(--bg-secondary, #f8f9fa);
padding: 15px;
border-radius: 6px;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: var(--accent-color, #007acc);
display: block;
}
.stat-label {
font-size: 14px;
color: var(--text-secondary, #666);
margin-top: 5px;
}
.data-source {
background: var(--bg-secondary, #f8f9fa);
border: 1px solid var(--border-color, #ddd);
border-radius: 6px;
padding: 15px;
margin: 10px 0;
}
.source-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.source-name {
font-weight: bold;
font-size: 16px;
}
.source-license {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.license-public {
background: #d4edda;
color: #155724;
}
.license-agpl {
background: #fff3cd;
color: #856404;
}
.source-url {
font-family: monospace;
font-size: 12px;
color: var(--text-secondary, #666);
word-break: break-all;
}
.loading {
text-align: center;
padding: 20px;
color: var(--text-secondary, #666);
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 6px;
margin: 10px 0;
}
.nav-header {
background: var(--header-bg, #2c3e50);
color: white;
padding: 15px;
margin-bottom: 20px;
}
.nav-header h1 {
margin: 0;
display: inline-block;
}
.nav-links {
float: right;
margin-top: 5px;
}
.nav-links a {
color: #ecf0f1;
text-decoration: none;
margin-left: 20px;
padding: 5px 10px;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav-links a:hover {
background-color: rgba(255,255,255,0.1);
}
/* Override main CSS to allow scrolling */
body {
overflow: auto !important;
height: auto !important;
}
</style>
</head>
<body>
<div class="nav-header">
<h1>SkyView Database Status</h1>
<div class="nav-links">
<a href="/">← Back to Map</a>
<a href="#" onclick="refreshData()">🔄 Refresh</a>
</div>
<div style="clear: both;"></div>
</div>
<div class="database-container">
<!-- Database Status -->
<div class="status-card">
<h2>📊 Database Statistics</h2>
<div id="database-stats" class="loading">Loading database statistics...</div>
</div>
<!-- External Data Sources -->
<div class="status-card">
<h2>📦 External Data Sources</h2>
<div id="data-sources" class="loading">Loading data sources...</div>
</div>
</div>
<script>
let dbStatusData = null;
let dataSourcesData = null;
async function loadDatabaseStats() {
try {
const response = await fetch('/api/database/status');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
dbStatusData = await response.json();
renderDatabaseStats();
} catch (error) {
document.getElementById('database-stats').innerHTML =
`<div class="error">Failed to load database statistics: ${error.message}</div>`;
}
}
async function loadDataSources() {
try {
const response = await fetch('/api/database/sources');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
dataSourcesData = await response.json();
renderDataSources();
} catch (error) {
document.getElementById('data-sources').innerHTML =
`<div class="error">Failed to load data sources: ${error.message}</div>`;
}
}
function renderDatabaseStats() {
if (!dbStatusData) return;
const container = document.getElementById('database-stats');
const history = dbStatusData.history || {};
const callsign = dbStatusData.callsign || {};
const referenceData = dbStatusData.reference_data || {};
// Format file size
const sizeFormatted = dbStatusData.size_mb ? `${dbStatusData.size_mb.toFixed(1)} MB` : 'Unknown';
// Format database path (show only filename for space)
const dbPath = dbStatusData.path || 'Unknown';
const dbFilename = dbPath.split('/').pop();
// Format last modified date
const lastModified = dbStatusData.modified ?
new Date(dbStatusData.modified * 1000).toLocaleString() : 'Unknown';
// Format efficiency percentage
const efficiency = dbStatusData.efficiency_percent !== undefined ?
`${dbStatusData.efficiency_percent.toFixed(1)}%` : 'Unknown';
container.innerHTML = `
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value">${history.total_records || 0}</span>
<div class="stat-label">Total Aircraft Records</div>
</div>
<div class="stat-item">
<span class="stat-value">${history.unique_aircraft || 0}</span>
<div class="stat-label">Unique Aircraft</div>
</div>
<div class="stat-item">
<span class="stat-value">${history.recent_records_24h || 0}</span>
<div class="stat-label">Records (Last 24h)</div>
</div>
<div class="stat-item">
<span class="stat-value">${callsign.cache_entries || 0}</span>
<div class="stat-label">Cached Callsigns</div>
</div>
<div class="stat-item">
<span class="stat-value">${sizeFormatted}</span>
<div class="stat-label">Database Size</div>
</div>
<div class="stat-item">
<span class="stat-value">${efficiency}</span>
<div class="stat-label">Storage Efficiency</div>
</div>
<div class="stat-item">
<span class="stat-value">${referenceData.airlines || 0}</span>
<div class="stat-label">Airlines Database</div>
</div>
<div class="stat-item">
<span class="stat-value">${referenceData.airports || 0}</span>
<div class="stat-label">Airports Database</div>
</div>
</div>
<div style="margin-top: 20px;">
<p><strong>Database Location:</strong> <code>${dbPath}</code></p>
<p><strong>Last Modified:</strong> ${lastModified}</p>
${dbStatusData.page_count ? `
<p><strong>Storage Details:</strong> ${dbStatusData.page_count.toLocaleString()} pages
(${dbStatusData.page_size} bytes each), ${dbStatusData.used_pages.toLocaleString()} used,
${dbStatusData.free_pages.toLocaleString()} free</p>
` : ''}
</div>
${history.oldest_record ? `
<div style="margin-top: 15px;">
<p><strong>Data Range:</strong> ${new Date(history.oldest_record * 1000).toLocaleDateString()}
to ${new Date(history.newest_record * 1000).toLocaleDateString()}</p>
</div>
` : ''}
`;
}
function renderDataSources() {
if (!dataSourcesData) return;
const container = document.getElementById('data-sources');
const available = dataSourcesData.available || [];
const loaded = dataSourcesData.loaded || [];
const loadedNames = new Set(loaded.map(s => s.name));
let html = '<h3>📥 Loaded Sources</h3>';
if (loaded.length === 0) {
html += '<p>No external data sources have been loaded yet. Use <code>skyview-data update</code> to load aviation reference data.</p>';
} else {
loaded.forEach(source => {
const licenseClass = source.license.includes('Public Domain') ? 'license-public' : 'license-agpl';
html += `
<div class="data-source">
<div class="source-header">
<span class="source-name">✅ ${source.name}</span>
<span class="source-license ${licenseClass}">${source.license}</span>
</div>
<div class="source-url">${source.url}</div>
</div>
`;
});
}
html += '<h3>📋 Available Sources</h3>';
available.forEach(source => {
const isLoaded = loadedNames.has(source.name);
const licenseClass = source.license.includes('Public Domain') ? 'license-public' : 'license-agpl';
const statusIcon = isLoaded ? '✅' : (source.requires_consent ? '⚠️' : '📦');
html += `
<div class="data-source">
<div class="source-header">
<span class="source-name">${statusIcon} ${source.name}</span>
<span class="source-license ${licenseClass}">${source.license}</span>
</div>
<div class="source-url">${source.url}</div>
${source.requires_consent ? '<p><small>⚠️ Requires license acceptance for use</small></p>' : ''}
${isLoaded ? '<p><small>✅ Currently loaded in database</small></p>' : ''}
</div>
`;
});
container.innerHTML = html;
}
function refreshData() {
document.getElementById('database-stats').innerHTML = '<div class="loading">Refreshing database statistics...</div>';
document.getElementById('data-sources').innerHTML = '<div class="loading">Refreshing data sources...</div>';
loadDatabaseStats();
loadDataSources();
}
// Load data on page load
document.addEventListener('DOMContentLoaded', function() {
loadDatabaseStats();
loadDataSources();
// Auto-refresh every 30 seconds
setInterval(() => {
loadDatabaseStats();
loadDataSources();
}, 30000);
});
</script>
</body>
</html>