restyle main page to be more sane
This commit is contained in:
+485
-357
@@ -5,396 +5,548 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>OSM Import Tools</title>
|
<title>OSM Import Tools</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
html, body {
|
||||||
box-sizing: border-box;
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #eef0f3;
|
||||||
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
/* ── Page shell ── */
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
.page {
|
||||||
background: #f5f5f5;
|
height: 100%;
|
||||||
padding: 20px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
/* ── Top bar ── */
|
||||||
max-width: 1200px;
|
.topbar {
|
||||||
margin: 0 auto;
|
display: flex;
|
||||||
background: white;
|
align-items: center;
|
||||||
padding: 30px;
|
gap: 12px;
|
||||||
border-radius: 8px;
|
padding: 10px 16px;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
background: #1a1a2e;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.topbar h1 {
|
||||||
color: #333;
|
font-size: 16px;
|
||||||
margin-bottom: 10px;
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.topbar-sep {
|
||||||
color: #666;
|
width: 1px;
|
||||||
margin-bottom: 30px;
|
height: 20px;
|
||||||
|
background: #ffffff30;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.btn-topbar {
|
||||||
margin-bottom: 30px;
|
padding: 5px 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.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: none;
|
||||||
border-radius: 6px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
transition: background 0.2s;
|
background: #ffffff18;
|
||||||
|
color: #ddd;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.script-button:hover {
|
.btn-topbar:hover { background: #ffffff30; }
|
||||||
background: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-button:disabled {
|
.btn-topbar.purple {
|
||||||
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;
|
background: #6f42c1;
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-link:hover {
|
.btn-topbar.purple:hover { background: #5a32a3; }
|
||||||
background: #5a32a3;
|
|
||||||
|
/* ── Main two-panel layout ── */
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-viewer {
|
/* ── Left panel (workflows) ── */
|
||||||
margin-top: 30px;
|
.left {
|
||||||
border-top: 2px solid #eee;
|
width: 340px;
|
||||||
padding-top: 20px;
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid #d8dbe0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-box {
|
/* County tabs */
|
||||||
background: #1e1e1e;
|
.county-tabs {
|
||||||
color: #d4d4d4;
|
display: flex;
|
||||||
padding: 15px;
|
flex-shrink: 0;
|
||||||
border-radius: 6px;
|
border-bottom: 1px solid #d8dbe0;
|
||||||
font-family: 'Courier New', monospace;
|
}
|
||||||
font-size: 12px;
|
|
||||||
max-height: 400px;
|
.county-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
background: #f4f5f7;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #888;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.county-tab:hover { color: #444; }
|
||||||
|
|
||||||
|
.county-tab.active.lake { color: #1a7a33; border-bottom-color: #28a745; background: white; }
|
||||||
|
.county-tab.active.sumter { color: #0d7a8f; border-bottom-color: #17a2b8; background: white; }
|
||||||
|
|
||||||
|
/* Scrollable workflow area */
|
||||||
|
.workflows {
|
||||||
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
white-space: pre-wrap;
|
padding: 14px 16px;
|
||||||
word-wrap: break-word;
|
display: flex;
|
||||||
display: none;
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-box.active {
|
/* Workflow group (Roads / Paths / Addresses) */
|
||||||
display: block;
|
.wf-group {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.07);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-message {
|
.wf-title {
|
||||||
padding: 10px 15px;
|
padding: 8px 14px;
|
||||||
border-radius: 6px;
|
font-size: 11px;
|
||||||
margin-bottom: 15px;
|
font-weight: 800;
|
||||||
display: none;
|
text-transform: uppercase;
|
||||||
}
|
letter-spacing: 0.7px;
|
||||||
|
|
||||||
.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;
|
color: #555;
|
||||||
margin-bottom: 10px;
|
background: #f8f9fa;
|
||||||
font-size: 16px;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-with-checkbox {
|
/* Steps */
|
||||||
|
.step {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
padding: 9px 14px;
|
||||||
|
border-bottom: 1px solid #f3f3f3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-with-checkbox label {
|
.step:last-child { border-bottom: none; }
|
||||||
|
|
||||||
|
.step-num {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e8eaf6;
|
||||||
|
color: #3949ab;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
justify-content: center;
|
||||||
font-size: 13px;
|
flex-shrink: 0;
|
||||||
color: #666;
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Run buttons */
|
||||||
|
.btn-run {
|
||||||
|
padding: 5px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
transition: filter 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-run:hover { filter: brightness(0.88); }
|
||||||
|
.btn-run:disabled { filter: brightness(0.5); cursor: not-allowed; }
|
||||||
|
|
||||||
|
.lake-active .btn-run { background: #28a745; }
|
||||||
|
.sumter-active .btn-run { background: #17a2b8; }
|
||||||
|
|
||||||
|
/* Force redownload */
|
||||||
|
.force-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #aaa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-with-checkbox input[type="checkbox"] {
|
/* Upload (paths) */
|
||||||
cursor: pointer;
|
.upload-input {
|
||||||
|
font-size: 11px;
|
||||||
|
width: 120px;
|
||||||
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-button {
|
/* N/A badge */
|
||||||
padding: 10px 20px;
|
.na {
|
||||||
background: #dc3545;
|
font-size: 10px;
|
||||||
color: white;
|
color: #ccc;
|
||||||
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;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-row input[type="file"] {
|
/* Data files strip */
|
||||||
font-size: 13px;
|
.files-strip {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-top: 1px solid #d8dbe0;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-strip-title {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
color: #aaa;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-link {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-link.lake { background: #28a745; }
|
||||||
|
.file-link.sumter { background: #17a2b8; }
|
||||||
|
|
||||||
|
/* ── Right panel (console) ── */
|
||||||
|
.right {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: #111;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-header span {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
padding: 3px 10px;
|
||||||
|
background: #c0392b;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel.active { display: inline-block; }
|
||||||
|
|
||||||
|
#status {
|
||||||
|
padding: 7px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status.success { background: #1e3a24; color: #6fcf7f; }
|
||||||
|
#status.error { background: #3a1e1e; color: #e07070; }
|
||||||
|
#status.info { background: #1e2e3a; color: #70b8e0; }
|
||||||
|
|
||||||
|
#logs {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #d4d4d4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-placeholder {
|
||||||
|
color: #444;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="page">
|
||||||
|
|
||||||
|
<!-- Top bar -->
|
||||||
|
<div class="topbar">
|
||||||
<h1>OSM Import Tools</h1>
|
<h1>OSM Import Tools</h1>
|
||||||
<p class="subtitle">Run data processing scripts and view results</p>
|
<div class="topbar-sep"></div>
|
||||||
|
<button class="btn-topbar" onclick="runScript('make-new-latest', '')">New Latest</button>
|
||||||
<div class="section">
|
<button class="btn-topbar" onclick="runScript('ls', '')">List Files</button>
|
||||||
<h2>Map Viewer</h2>
|
<div style="flex:1"></div>
|
||||||
<a href="/map" class="map-link">Open GeoJSON Map Viewer</a>
|
<a href="/map" class="btn-topbar purple">Open Map Viewer</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="main">
|
||||||
<h2>Data Processing Scripts</h2>
|
|
||||||
|
|
||||||
{% for category, script_names in scripts_by_category.items() %}
|
<!-- Left: workflows -->
|
||||||
<div class="county-group">
|
<div class="left" id="leftPanel">
|
||||||
<h3>{{ category }}</h3>
|
|
||||||
|
|
||||||
{% for script_name in script_names %}
|
<div class="county-tabs">
|
||||||
{% if script_name in script_map %}
|
<button class="county-tab lake active" onclick="selectCounty('lake')">Lake County</button>
|
||||||
{% set script_config = script_map[script_name] %}
|
<button class="county-tab sumter" onclick="selectCounty('sumter')">Sumter County</button>
|
||||||
|
|
||||||
{% 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>
|
</div>
|
||||||
{% elif script_config is mapping %}
|
|
||||||
{# County-specific commands #}
|
<div class="workflows">
|
||||||
<div class="button-grid">
|
|
||||||
{% if 'lake' in script_config %}
|
<!-- Roads -->
|
||||||
{% set lake_config = script_config['lake'] %}
|
<div class="wf-group">
|
||||||
{% if lake_config is mapping and lake_config.get('type') == 'upload' %}
|
<div class="wf-title">Roads</div>
|
||||||
<div class="upload-row">
|
<div class="step">
|
||||||
<input type="file" id="upload-lake-{{ script_name }}" accept=".zip">
|
<div class="step-num">1</div>
|
||||||
<button class="script-button lake" onclick="uploadFile('upload-lake-{{ script_name }}', '{{ lake_config['dest'] }}')">
|
<div class="step-label">Download county data</div>
|
||||||
{{ script_name|replace('-', ' ')|title }} (Lake)
|
<div class="step-actions">
|
||||||
</button>
|
<button class="btn-run" onclick="runScript('download-county-roads')">Run</button>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
<div class="button-with-checkbox">
|
<div class="step">
|
||||||
<button class="script-button lake" onclick="runScript('{{ script_name }}', 'lake')">
|
<div class="step-num">2</div>
|
||||||
{{ script_name|replace('-', ' ')|title }} (Lake)
|
<div class="step-label">Download OSM roads</div>
|
||||||
</button>
|
<div class="step-actions">
|
||||||
{% if script_name == 'diff-addresses' %}
|
<button class="btn-run" onclick="runScript('download-osm-roads')">Run</button>
|
||||||
<label>
|
</div>
|
||||||
<input type="checkbox" id="force-redownload-lake">
|
</div>
|
||||||
Force OSM Redownload
|
<div class="step">
|
||||||
|
<div class="step-num">3</div>
|
||||||
|
<div class="step-label">Convert to GeoJSON</div>
|
||||||
|
<div class="step-actions">
|
||||||
|
<button class="btn-run" onclick="runScript('convert-roads')">Run</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">4</div>
|
||||||
|
<div class="step-label">Diff vs OSM</div>
|
||||||
|
<div class="step-actions">
|
||||||
|
<button class="btn-run" onclick="runScript('diff-roads')">Run</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Paths (Sumter only) -->
|
||||||
|
<div class="wf-group" id="paths-group">
|
||||||
|
<div class="wf-title">Paths</div>
|
||||||
|
<div class="step" id="paths-step-1">
|
||||||
|
<div class="step-num">1</div>
|
||||||
|
<div class="step-label">Upload county paths</div>
|
||||||
|
<div class="step-actions" id="paths-step-1-actions">
|
||||||
|
<!-- filled by selectCounty() -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">2</div>
|
||||||
|
<div class="step-label">Download OSM paths</div>
|
||||||
|
<div class="step-actions" id="paths-step-2-actions">
|
||||||
|
<!-- filled by selectCounty() -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">3</div>
|
||||||
|
<div class="step-label">Convert to GeoJSON</div>
|
||||||
|
<div class="step-actions" id="paths-step-3-actions">
|
||||||
|
<!-- filled by selectCounty() -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">4</div>
|
||||||
|
<div class="step-label">Diff vs OSM</div>
|
||||||
|
<div class="step-actions" id="paths-step-4-actions">
|
||||||
|
<!-- filled by selectCounty() -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Addresses -->
|
||||||
|
<div class="wf-group">
|
||||||
|
<div class="wf-title">Addresses</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">1</div>
|
||||||
|
<div class="step-label">Download county data</div>
|
||||||
|
<div class="step-actions">
|
||||||
|
<button class="btn-run" onclick="runScript('download-county-addresses')">Run</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">2</div>
|
||||||
|
<div class="step-label">Download OSM addresses</div>
|
||||||
|
<div class="step-actions">
|
||||||
|
<button class="btn-run" onclick="runScript('download-osm-addresses')">Run</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">3</div>
|
||||||
|
<div class="step-label">Convert to GeoJSON</div>
|
||||||
|
<div class="step-actions">
|
||||||
|
<button class="btn-run" onclick="runScript('convert-addresses')">Run</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">4</div>
|
||||||
|
<div class="step-label">Diff vs OSM</div>
|
||||||
|
<div class="step-actions">
|
||||||
|
<button class="btn-run" onclick="runScript('diff-addresses')">Run</button>
|
||||||
|
<label class="force-label">
|
||||||
|
<input type="checkbox" id="force-redownload"> Force redownload
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</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>
|
</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>
|
||||||
|
|
||||||
<div class="section">
|
</div><!-- /workflows -->
|
||||||
<h2>Data Downloads</h2>
|
|
||||||
|
|
||||||
|
<!-- Data files strip -->
|
||||||
|
{% if data_files %}
|
||||||
|
<div class="files-strip">
|
||||||
|
<div class="files-strip-title">Downloaded files</div>
|
||||||
|
<div class="file-links">
|
||||||
{% for county, file_names in data_files.items() %}
|
{% for county, file_names in data_files.items() %}
|
||||||
<div class="county-group">
|
|
||||||
<h3>{{ county|title }}</h3>
|
|
||||||
|
|
||||||
{% for file_name in file_names %}
|
{% for file_name in file_names %}
|
||||||
<div class="button-grid">
|
<a class="file-link {% if 'lake' in county %}lake{% else %}sumter{% endif %}"
|
||||||
{% if 'lake' in county %}
|
href="/data/latest/{{ county }}/{{ file_name }}"
|
||||||
<a class="script-button lake" href="/data/latest/{{ county }}/{{ file_name }}">
|
title="{{ county }}/{{ file_name }}">{{ file_name }}</a>
|
||||||
{{ file_name }}
|
{% endfor %}
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if 'sumter' in county %}
|
|
||||||
<a class="script-button sumter" href="/data/latest/{{ county }}/{{ file_name }}">
|
|
||||||
{{ file_name }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div><!-- /left -->
|
||||||
|
|
||||||
|
<!-- Right: console -->
|
||||||
|
<div class="right">
|
||||||
|
<div class="console-header">
|
||||||
|
<span>Output</span>
|
||||||
|
<button id="cancelButton" class="btn-cancel" onclick="cancelJob()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
<div id="status"></div>
|
||||||
|
<div id="logs"><span class="log-placeholder">Run a step to see output here.</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="log-viewer">
|
</div><!-- /main -->
|
||||||
<h2>Script Output</h2>
|
</div><!-- /page -->
|
||||||
<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>
|
<script>
|
||||||
|
let activeCounty = 'lake';
|
||||||
let currentJobId = null;
|
let currentJobId = null;
|
||||||
let logCheckInterval = null;
|
let logCheckInterval = null;
|
||||||
|
|
||||||
function showStatus(message, type) {
|
function selectCounty(county) {
|
||||||
const statusEl = document.getElementById('status');
|
activeCounty = county;
|
||||||
statusEl.textContent = message;
|
|
||||||
statusEl.className = `status-message ${type}`;
|
// Update tab styles
|
||||||
statusEl.style.display = 'block';
|
document.querySelectorAll('.county-tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelector(`.county-tab.${county}`).classList.add('active');
|
||||||
|
|
||||||
|
// Update left panel class for button color theming
|
||||||
|
const panel = document.getElementById('leftPanel');
|
||||||
|
panel.classList.toggle('lake-active', county === 'lake');
|
||||||
|
panel.classList.toggle('sumter-active', county === 'sumter');
|
||||||
|
|
||||||
|
// Update paths section availability
|
||||||
|
renderPathsActions(county);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runScript(scriptName, county) {
|
function renderPathsActions(county) {
|
||||||
const logsEl = document.getElementById('logs');
|
if (county === 'lake') {
|
||||||
logsEl.textContent = 'Starting script...\n';
|
['1','2','3','4'].forEach(n => {
|
||||||
logsEl.classList.add('active');
|
document.getElementById(`paths-step-${n}-actions`).innerHTML =
|
||||||
showStatus(`Running ${scriptName} for ${county}...`, 'info');
|
'<span class="na">Not available for Lake</span>';
|
||||||
|
|
||||||
// Disable all buttons
|
|
||||||
document.querySelectorAll('.script-button').forEach(btn => {
|
|
||||||
btn.disabled = true;
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Step 1: upload
|
||||||
|
document.getElementById('paths-step-1-actions').innerHTML =
|
||||||
|
'<input type="file" id="upload-sumter-paths" class="upload-input" accept=".zip">' +
|
||||||
|
'<button class="btn-run" onclick="uploadFile(\'upload-sumter-paths\', \'/data/latest/sumter/paths.shp.zip\')">Upload</button>';
|
||||||
|
// Steps 2-4: run buttons
|
||||||
|
[
|
||||||
|
['2', 'download-osm-paths'],
|
||||||
|
['3', 'convert-paths'],
|
||||||
|
['4', 'diff-paths'],
|
||||||
|
].forEach(([n, script]) => {
|
||||||
|
document.getElementById(`paths-step-${n}-actions`).innerHTML =
|
||||||
|
`<button class="btn-run" onclick="runScript('${script}')">Run</button>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show cancel button
|
// Init
|
||||||
|
selectCounty('lake');
|
||||||
|
|
||||||
|
/* ── Script runner ── */
|
||||||
|
function runScript(scriptName) {
|
||||||
|
const logsEl = document.getElementById('logs');
|
||||||
|
logsEl.textContent = '';
|
||||||
|
showStatus(`Running: ${scriptName} (${activeCounty})`, 'info');
|
||||||
|
disableButtons();
|
||||||
document.getElementById('cancelButton').classList.add('active');
|
document.getElementById('cancelButton').classList.add('active');
|
||||||
|
|
||||||
// Check if force redownload is checked (for diff-addresses)
|
|
||||||
let forceDownload = false;
|
let forceDownload = false;
|
||||||
if (scriptName === 'diff-addresses') {
|
if (scriptName === 'diff-addresses') {
|
||||||
const checkboxId = `force-redownload-${county}`;
|
const cb = document.getElementById('force-redownload');
|
||||||
const checkbox = document.getElementById(checkboxId);
|
if (cb) forceDownload = cb.checked;
|
||||||
if (checkbox) {
|
|
||||||
forceDownload = checkbox.checked;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch('/api/run-script', {
|
fetch('/api/run-script', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json'
|
body: JSON.stringify({ script: scriptName, county: activeCounty, forceDownload })
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
script: scriptName,
|
|
||||||
county: county,
|
|
||||||
forceDownload: forceDownload
|
|
||||||
})
|
})
|
||||||
})
|
.then(r => r.json())
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
showStatus(`Error: ${data.error}`, 'error');
|
showStatus(`Error: ${data.error}`, 'error');
|
||||||
@@ -402,86 +554,55 @@
|
|||||||
document.getElementById('cancelButton').classList.remove('active');
|
document.getElementById('cancelButton').classList.remove('active');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentJobId = data.job_id;
|
currentJobId = data.job_id;
|
||||||
showStatus(`Script started (Job ID: ${data.job_id})`, 'success');
|
if (logCheckInterval) clearInterval(logCheckInterval);
|
||||||
|
|
||||||
// Start polling for logs
|
|
||||||
if (logCheckInterval) {
|
|
||||||
clearInterval(logCheckInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
logCheckInterval = setInterval(checkJobStatus, 1000);
|
logCheckInterval = setInterval(checkJobStatus, 1000);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(err => {
|
||||||
showStatus(`Error: ${error.message}`, 'error');
|
showStatus(`Error: ${err.message}`, 'error');
|
||||||
enableButtons();
|
enableButtons();
|
||||||
document.getElementById('cancelButton').classList.remove('active');
|
document.getElementById('cancelButton').classList.remove('active');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelJob() {
|
function cancelJob() {
|
||||||
if (!currentJobId) return;
|
if (!currentJobId || !confirm('Cancel the running job?')) return;
|
||||||
|
|
||||||
if (!confirm('Are you sure you want to cancel this job?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch('/api/cancel-job', {
|
fetch('/api/cancel-job', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json'
|
body: JSON.stringify({ job_id: currentJobId })
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
job_id: currentJobId
|
|
||||||
})
|
})
|
||||||
})
|
.then(r => r.json())
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showStatus('Job cancelled', 'info');
|
showStatus('Cancelled.', 'info');
|
||||||
clearInterval(logCheckInterval);
|
clearInterval(logCheckInterval);
|
||||||
enableButtons();
|
enableButtons();
|
||||||
document.getElementById('cancelButton').classList.remove('active');
|
document.getElementById('cancelButton').classList.remove('active');
|
||||||
currentJobId = null;
|
currentJobId = null;
|
||||||
} else {
|
} else {
|
||||||
showStatus(`Error cancelling job: ${data.error}`, 'error');
|
showStatus(`Cancel failed: ${data.error}`, 'error');
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showStatus(`Error cancelling job: ${error.message}`, 'error');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkJobStatus() {
|
function checkJobStatus() {
|
||||||
if (!currentJobId) return;
|
if (!currentJobId) return;
|
||||||
|
|
||||||
fetch(`/api/job-status/${currentJobId}`)
|
fetch(`/api/job-status/${currentJobId}`)
|
||||||
.then(response => response.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const logsEl = document.getElementById('logs');
|
const logsEl = document.getElementById('logs');
|
||||||
logsEl.textContent = data.logs.join('');
|
logsEl.textContent = data.logs.join('');
|
||||||
|
|
||||||
// Auto-scroll to bottom
|
|
||||||
logsEl.scrollTop = logsEl.scrollHeight;
|
logsEl.scrollTop = logsEl.scrollHeight;
|
||||||
|
|
||||||
if (!data.running) {
|
if (!data.running) {
|
||||||
clearInterval(logCheckInterval);
|
clearInterval(logCheckInterval);
|
||||||
showStatus('Script completed', 'success');
|
showStatus('Done.', 'success');
|
||||||
enableButtons();
|
enableButtons();
|
||||||
document.getElementById('cancelButton').classList.remove('active');
|
document.getElementById('cancelButton').classList.remove('active');
|
||||||
currentJobId = null;
|
currentJobId = null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(err => console.error('Status check error:', err));
|
||||||
console.error('Error checking job status:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableButtons() {
|
|
||||||
document.querySelectorAll('.script-button').forEach(btn => {
|
|
||||||
btn.disabled = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadFile(inputId, dest) {
|
function uploadFile(inputId, dest) {
|
||||||
@@ -490,20 +611,16 @@
|
|||||||
showStatus('Please select a file first.', 'error');
|
showStatus('Please select a file first.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('dest', dest);
|
formData.append('dest', dest);
|
||||||
|
|
||||||
showStatus(`Uploading ${file.name}...`, 'info');
|
showStatus(`Uploading ${file.name}...`, 'info');
|
||||||
document.querySelectorAll('.script-button').forEach(btn => btn.disabled = true);
|
disableButtons();
|
||||||
|
|
||||||
fetch('/api/upload-file', {
|
fetch('/api/upload-file', { method: 'POST', body: formData })
|
||||||
method: 'POST',
|
.then(r => r.json())
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
showStatus(`Error: ${data.error}`, 'error');
|
showStatus(`Error: ${data.error}`, 'error');
|
||||||
@@ -512,12 +629,23 @@
|
|||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(err => showStatus(`Error: ${err.message}`, 'error'))
|
||||||
showStatus(`Error: ${error.message}`, 'error');
|
.finally(() => enableButtons());
|
||||||
})
|
}
|
||||||
.finally(() => {
|
|
||||||
enableButtons();
|
function showStatus(msg, type) {
|
||||||
});
|
const el = document.getElementById('status');
|
||||||
|
el.textContent = msg;
|
||||||
|
el.className = type;
|
||||||
|
el.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableButtons() {
|
||||||
|
document.querySelectorAll('.btn-run').forEach(b => b.disabled = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableButtons() {
|
||||||
|
document.querySelectorAll('.btn-run').forEach(b => b.disabled = false);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user