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:
parent
96f90b1543
commit
ce7710d1b4
1 changed files with 360 additions and 0 deletions
360
assets/static/database.html
Normal file
360
assets/static/database.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue