Files
osm-import-tools/web/templates/index.html
T
2026-04-03 22:27:44 -07:00

525 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OSM Import Tools</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.section {
margin-bottom: 30px;
}
.section h2 {
color: #444;
margin-bottom: 15px;
font-size: 20px;
}
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.script-button {
padding: 15px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
text-decoration: none;
}
.script-button:hover {
background: #0056b3;
}
.script-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.script-button.lake {
background: #28a745;
}
.script-button.lake:hover {
background: #218838;
}
.script-button.sumter {
background: #17a2b8;
}
.script-button.sumter:hover {
background: #138496;
}
.map-link {
display: inline-block;
padding: 15px 30px;
background: #6f42c1;
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
transition: background 0.2s;
}
.map-link:hover {
background: #5a32a3;
}
.log-viewer {
margin-top: 30px;
border-top: 2px solid #eee;
padding-top: 20px;
}
.log-box {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
display: none;
}
.log-box.active {
display: block;
}
.status-message {
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 15px;
display: none;
}
.status-message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status-message.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.county-group {
margin-bottom: 25px;
}
.county-group h3 {
color: #555;
margin-bottom: 10px;
font-size: 16px;
}
.button-with-checkbox {
display: flex;
align-items: center;
gap: 10px;
}
.button-with-checkbox label {
display: flex;
align-items: center;
gap: 5px;
font-size: 13px;
color: #666;
cursor: pointer;
white-space: nowrap;
}
.button-with-checkbox input[type="checkbox"] {
cursor: pointer;
}
.cancel-button {
padding: 10px 20px;
background: #dc3545;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
display: none;
}
.cancel-button:hover {
background: #c82333;
}
.cancel-button.active {
display: inline-block;
}
.upload-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.upload-row span.unavailable {
color: #999;
font-size: 13px;
font-style: italic;
}
.upload-row input[type="file"] {
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<h1>OSM Import Tools</h1>
<p class="subtitle">Run data processing scripts and view results</p>
<div class="section">
<h2>Map Viewer</h2>
<a href="/map" class="map-link">Open GeoJSON Map Viewer</a>
</div>
<div class="section">
<h2>Data Processing Scripts</h2>
{% for category, script_names in scripts_by_category.items() %}
<div class="county-group">
<h3>{{ category }}</h3>
{% for script_name in script_names %}
{% if script_name in script_map %}
{% set script_config = script_map[script_name] %}
{% if script_config is string %}
{# Simple command with no county selection #}
<div class="button-grid">
<button class="script-button" onclick="runScript('{{ script_name }}', '')">
{{ script_name|replace('-', ' ')|title }}
</button>
</div>
{% elif script_config is mapping %}
{# County-specific commands #}
<div class="button-grid">
{% if 'lake' in script_config %}
{% set lake_config = script_config['lake'] %}
{% if lake_config is mapping and lake_config.get('type') == 'upload' %}
<div class="upload-row">
<input type="file" id="upload-lake-{{ script_name }}" accept=".zip">
<button class="script-button lake" onclick="uploadFile('upload-lake-{{ script_name }}', '{{ lake_config['dest'] }}')">
{{ script_name|replace('-', ' ')|title }} (Lake)
</button>
</div>
{% else %}
<div class="button-with-checkbox">
<button class="script-button lake" onclick="runScript('{{ script_name }}', 'lake')">
{{ script_name|replace('-', ' ')|title }} (Lake)
</button>
{% if script_name == 'diff-addresses' %}
<label>
<input type="checkbox" id="force-redownload-lake">
Force OSM Redownload
</label>
{% endif %}
</div>
{% endif %}
{% endif %}
{% if 'sumter' in script_config %}
{% set sumter_config = script_config['sumter'] %}
{% if sumter_config is mapping and sumter_config.get('type') == 'upload' %}
<div class="upload-row">
<input type="file" id="upload-sumter-{{ script_name }}" accept=".zip">
<button class="script-button sumter" onclick="uploadFile('upload-sumter-{{ script_name }}', '{{ sumter_config['dest'] }}')">
{{ script_name|replace('-', ' ')|title }} (Sumter)
</button>
</div>
{% else %}
<div class="button-with-checkbox">
<button class="script-button sumter" onclick="runScript('{{ script_name }}', 'sumter')">
{{ script_name|replace('-', ' ')|title }} (Sumter)
</button>
{% if script_name == 'diff-addresses' %}
<label>
<input type="checkbox" id="force-redownload-sumter">
Force OSM Redownload
</label>
{% endif %}
</div>
{% endif %}
{% else %}
{% if script_name == 'download-county-paths' %}
<div class="upload-row">
<span class="unavailable">Download County Paths (Lake): not available</span>
</div>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endif %}
{% endfor %}
</div>
{% endfor %}
</div>
<div class="section">
<h2>Data Downloads</h2>
{% for county, file_names in data_files.items() %}
<div class="county-group">
<h3>{{ county|title }}</h3>
{% for file_name in file_names %}
<div class="button-grid">
{% if 'lake' in county %}
<a class="script-button lake" href="/data/latest/{{ county }}/{{ file_name }}">
{{ file_name }}
</a>
{% endif %}
{% if 'sumter' in county %}
<a class="script-button sumter" href="/data/latest/{{ county }}/{{ file_name }}">
{{ file_name }}
</a>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="log-viewer">
<h2>Script Output</h2>
<div id="status" class="status-message"></div>
<button id="cancelButton" class="cancel-button" onclick="cancelJob()">Cancel Job</button>
<div id="logs" class="log-box"></div>
</div>
</div>
<script>
let currentJobId = null;
let logCheckInterval = null;
function showStatus(message, type) {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
statusEl.className = `status-message ${type}`;
statusEl.style.display = 'block';
}
function runScript(scriptName, county) {
const logsEl = document.getElementById('logs');
logsEl.textContent = 'Starting script...\n';
logsEl.classList.add('active');
showStatus(`Running ${scriptName} for ${county}...`, 'info');
// Disable all buttons
document.querySelectorAll('.script-button').forEach(btn => {
btn.disabled = true;
});
// Show cancel button
document.getElementById('cancelButton').classList.add('active');
// Check if force redownload is checked (for diff-addresses)
let forceDownload = false;
if (scriptName === 'diff-addresses') {
const checkboxId = `force-redownload-${county}`;
const checkbox = document.getElementById(checkboxId);
if (checkbox) {
forceDownload = checkbox.checked;
}
}
fetch('/api/run-script', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
script: scriptName,
county: county,
forceDownload: forceDownload
})
})
.then(response => response.json())
.then(data => {
if (data.error) {
showStatus(`Error: ${data.error}`, 'error');
enableButtons();
document.getElementById('cancelButton').classList.remove('active');
return;
}
currentJobId = data.job_id;
showStatus(`Script started (Job ID: ${data.job_id})`, 'success');
// Start polling for logs
if (logCheckInterval) {
clearInterval(logCheckInterval);
}
logCheckInterval = setInterval(checkJobStatus, 1000);
})
.catch(error => {
showStatus(`Error: ${error.message}`, 'error');
enableButtons();
document.getElementById('cancelButton').classList.remove('active');
});
}
function cancelJob() {
if (!currentJobId) return;
if (!confirm('Are you sure you want to cancel this job?')) {
return;
}
fetch('/api/cancel-job', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
job_id: currentJobId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showStatus('Job cancelled', 'info');
clearInterval(logCheckInterval);
enableButtons();
document.getElementById('cancelButton').classList.remove('active');
currentJobId = null;
} else {
showStatus(`Error cancelling job: ${data.error}`, 'error');
}
})
.catch(error => {
showStatus(`Error cancelling job: ${error.message}`, 'error');
});
}
function checkJobStatus() {
if (!currentJobId) return;
fetch(`/api/job-status/${currentJobId}`)
.then(response => response.json())
.then(data => {
const logsEl = document.getElementById('logs');
logsEl.textContent = data.logs.join('');
// Auto-scroll to bottom
logsEl.scrollTop = logsEl.scrollHeight;
if (!data.running) {
clearInterval(logCheckInterval);
showStatus('Script completed', 'success');
enableButtons();
document.getElementById('cancelButton').classList.remove('active');
currentJobId = null;
}
})
.catch(error => {
console.error('Error checking job status:', error);
});
}
function enableButtons() {
document.querySelectorAll('.script-button').forEach(btn => {
btn.disabled = false;
});
}
function uploadFile(inputId, dest) {
const input = document.getElementById(inputId);
if (!input || !input.files.length) {
showStatus('Please select a file first.', 'error');
return;
}
const file = input.files[0];
const formData = new FormData();
formData.append('file', file);
formData.append('dest', dest);
showStatus(`Uploading ${file.name}...`, 'info');
document.querySelectorAll('.script-button').forEach(btn => btn.disabled = true);
fetch('/api/upload-file', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
showStatus(`Error: ${data.error}`, 'error');
} else {
showStatus(`Uploaded to ${data.path}`, 'success');
input.value = '';
}
})
.catch(error => {
showStatus(`Error: ${error.message}`, 'error');
})
.finally(() => {
enableButtons();
});
}
</script>
</body>
</html>