Alpha stage commit

This commit is contained in:
2026-07-01 23:29:51 +02:00
parent b583bb1233
commit e23387738b
84 changed files with 40807 additions and 326 deletions

329
app/templates/index.html Normal file
View File

@@ -0,0 +1,329 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mobility Workbench</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="" />
<link rel="stylesheet" href="/static/style.css?v=20260701-harmonizer-module" />
</head>
<body>
<header>
<div>
<h1>Mobility Workbench</h1>
<p>Harmonized transit, mapping data, route layer, map review, and journey tests.</p>
</div>
<div class="actions">
<button id="refreshBtn">Refresh</button>
</div>
</header>
<main>
<aside>
<div class="sidebar-content">
<details class="card sidebar-section" data-sidebar-section="stats" open>
<summary><h2>Stats</h2></summary>
<div class="sidebar-section-body">
<div id="stats" class="stats"></div>
</div>
</details>
<details class="card sidebar-section" data-sidebar-section="qa" open>
<summary><h2>QA</h2></summary>
<div class="sidebar-section-body">
<div class="qa-toolbar">
<button type="button" id="refreshQaBtn">Refresh QA</button>
</div>
<div id="qaDashboard" class="qa-dashboard muted">No QA loaded.</div>
</div>
</details>
<details class="card sidebar-section" data-sidebar-section="jobs" open>
<summary><h2>Jobs</h2></summary>
<div class="sidebar-section-body">
<div id="jobs" class="jobs muted">No jobs loaded.</div>
</div>
</details>
<details class="card sidebar-section" data-sidebar-section="harmonization" open>
<summary><h2>GTFS Harmonization</h2></summary>
<div class="sidebar-section-body">
<details class="nested-section" data-sidebar-section="add-gtfs-source">
<summary><h3>Add GTFS source</h3></summary>
<div class="nested-section-body">
<form id="sourceForm">
<input name="catalog_entry_id" type="hidden" />
<input name="kind" type="hidden" value="gtfs" />
<label>Name <input name="name" required placeholder="DELFI / national GTFS" /></label>
<label>URL or path <input name="url" required placeholder="https://.../feed.zip or ./data/feed.zip" /></label>
<label>Country <input name="country" placeholder="DE" maxlength="8" /></label>
<label>License <input name="license" placeholder="ODbL / CC-BY / unknown" /></label>
<button type="submit">Add GTFS source</button>
</form>
</div>
</details>
<details class="nested-section source-catalog-card" data-sidebar-section="source-catalog">
<summary><h3>Transit source catalog</h3></summary>
<div class="nested-section-body">
<div id="sourceCatalogSummary" class="muted"></div>
<div class="filter-row source-catalog-filter">
<input id="sourceCatalogSearch" placeholder="Search catalog" />
<input id="sourceCatalogCountry" placeholder="Country" />
<select id="sourceCatalogPriority">
<option value="">all priorities</option>
<option value="P0">P0</option>
<option value="P0 fallback">P0 fallback</option>
<option value="P1">P1</option>
<option value="P2">P2</option>
<option value="P3">P3</option>
<option value="P4">P4</option>
<option value="P5">P5</option>
</select>
</div>
<div class="source-catalog-actions">
<button type="button" id="importSourceCatalogBtn">Import catalog</button>
<button type="button" id="importIngestableSourcesBtn">Import ingestable seeds</button>
</div>
<div id="sourceCatalog"></div>
</div>
</details>
<details class="nested-section" data-sidebar-section="gtfs-feed-qa" open>
<summary><h3>Feed QA</h3></summary>
<div class="nested-section-body">
<div class="qa-toolbar">
<button type="button" id="refreshGtfsHarmonizationBtn">Refresh feeds</button>
</div>
<div id="gtfsHarmonizationInventory" class="harmonization-inventory muted">No GTFS feed QA loaded.</div>
</div>
</details>
<details class="nested-section" data-sidebar-section="gtfs-source-management" open>
<summary><h3>GTFS source library</h3></summary>
<div class="nested-section-body">
<div class="filter-row">
<input id="sourceSearch" placeholder="Filter GTFS sources" />
</div>
<div id="sources"></div>
</div>
</details>
</div>
</details>
<details class="card sidebar-section" data-sidebar-section="mapping" open>
<summary><h2>Mapping Data</h2></summary>
<div class="sidebar-section-body">
<details class="nested-section" data-sidebar-section="add-map-source">
<summary><h3>Add map source</h3></summary>
<div class="nested-section-body">
<form id="mappingSourceForm">
<input name="catalog_entry_id" type="hidden" />
<label>Name <input name="name" required placeholder="Germany OSM PBF" /></label>
<label>Kind
<select name="kind">
<option value="osm_pbf">OSM PBF extract</option>
<option value="osm_geojson">OSM transport GeoJSON</option>
<option value="osm_diff">OSM change diff</option>
</select>
</label>
<label>URL or path <input name="url" required placeholder="https://.../latest.osm.pbf or ./data/routes.geojson" /></label>
<label>Country <input name="country" placeholder="DE" maxlength="8" /></label>
<label>License <input name="license" placeholder="ODbL / CC-BY / unknown" /></label>
<button type="submit">Add map source</button>
</form>
</div>
</details>
<details class="nested-section source-catalog-card" data-sidebar-section="geofabrik">
<summary><h3>Geofabrik OSM</h3></summary>
<div class="nested-section-body">
<div class="filter-row geofabrik-filter">
<input id="geofabrikSearch" placeholder="Berlin, Germany, Hamburg" />
<button type="button" id="geofabrikSearchBtn">Search</button>
</div>
<label class="inline-check"><input id="geofabrikDiffSource" type="checkbox" checked /> add diff source metadata</label>
<div id="geofabrikResults" class="dataset-search-results muted">Search Geofabrik extracts, then add or import one as an OSM PBF source.</div>
</div>
</details>
<details class="nested-section" data-sidebar-section="mapping-source-management" open>
<summary><h3>Map source library</h3></summary>
<div class="nested-section-body">
<div class="filter-row">
<input id="mappingSourceSearch" placeholder="Filter map sources" />
<select id="mappingSourceKindFilter">
<option value="">all map kinds</option>
<option value="osm_geojson">OSM GeoJSON</option>
<option value="osm_pbf">OSM PBF</option>
<option value="osm_diff">OSM diff</option>
</select>
</div>
<div id="mappingSources"></div>
</div>
</details>
</div>
</details>
<details class="card sidebar-section" data-sidebar-section="datasets" open>
<summary><h2>Datasets</h2></summary>
<div class="sidebar-section-body">
<details class="nested-section" data-sidebar-section="dataset-pipeline" open>
<summary><h3>Derivation pipeline</h3></summary>
<div class="nested-section-body">
<div class="workflow-actions">
<button id="runMatchBtn" type="button">Run matcher</button>
<button id="buildRouteLayerBtn" type="button">Build route layer</button>
<button id="loadSampleBtn" type="button">Reset sample</button>
</div>
</div>
</details>
<details class="nested-section" data-sidebar-section="dataset-search">
<summary><h3>Dataset search</h3></summary>
<div class="nested-section-body">
<form id="datasetSearchForm" class="dataset-search-form">
<input id="datasetSearchQuery" placeholder="Route, line, stop, shape ID" autocomplete="off" />
<div class="filter-row">
<label class="inline-check"><input id="datasetSearchActiveOnly" type="checkbox" checked /> active only</label>
<button type="submit">Search</button>
</div>
</form>
<div id="datasetSearchResults" class="dataset-search-results muted">Search all imported datasets by label, route ID, and route-layer reference.</div>
</div>
</details>
<details class="nested-section matches-card" data-sidebar-section="route-matches">
<summary><h3>Route matches</h3></summary>
<div class="nested-section-body">
<div class="filter-row">
<select id="matchStatusFilter">
<option value="">all</option>
<option value="matched">matched</option>
<option value="probable">probable</option>
<option value="weak">weak</option>
<option value="missing">missing</option>
<option value="accepted">accepted</option>
<option value="rejected">rejected</option>
</select>
<button id="reloadMatchesBtn">Reload</button>
</div>
<div id="matches"></div>
</div>
</details>
<details class="nested-section" data-sidebar-section="maintenance">
<summary><h3>Maintenance</h3></summary>
<div class="nested-section-body">
<div class="maintenance-grid">
<button type="button" data-admin-action="init-db">Init DB</button>
<button type="button" data-admin-action="backfill-gtfs-shapes">Backfill GTFS shapes</button>
<button type="button" data-admin-action="prune-cache-dry">Check cache</button>
<button type="button" data-admin-action="prune-cache">Prune cache</button>
<button type="button" data-admin-action="prune-inactive-dry">Check inactive</button>
<button type="button" data-admin-action="prune-inactive">Prune inactive</button>
<button type="button" data-admin-action="vacuum-db">Vacuum DB</button>
<button type="button" class="danger" data-admin-action="reset-db">Reset DB</button>
</div>
<div id="adminStatus" class="admin-status muted"></div>
</div>
</details>
</div>
</details>
<details class="card sidebar-section" data-sidebar-section="layers" open>
<summary><h2>Layers</h2></summary>
<div class="sidebar-section-body">
<div class="preset-row">
<button type="button" data-layer-preset="network">Network</button>
<button type="button" data-layer-preset="review">Matched/unmatched</button>
<button type="button" data-layer-preset="unmatched">Unmatched</button>
<button type="button" data-layer-preset="all">All</button>
</div>
<div id="layerControls" class="layer-controls"></div>
<div id="mapStatus" class="map-status muted"></div>
</div>
</details>
</div>
<button id="sidebarCollapseBtn" class="sidebar-collapse-handle" type="button" aria-label="Collapse left panel" title="Collapse left panel" aria-expanded="true"></button>
</aside>
<section class="map-panel">
<div id="map"></div>
<div id="mapLoading" class="map-loading" hidden>
<span class="spinner" aria-hidden="true"></span>
<span id="mapLoadingText">Loading map layers...</span>
</div>
<section class="map-floating journey-card">
<h2>Journey</h2>
<form id="journeyForm">
<div id="journeyTransitSnapshot" class="journey-snapshot muted">Transit snapshot loading...</div>
<label>From <input id="journeyFromQuery" placeholder="Hauptbahnhof" autocomplete="off" /></label>
<input id="journeyFromStop" type="hidden" />
<div id="journeyFromSuggestions" class="stop-suggestions"></div>
<button type="button" id="journeySwapBtn" class="journey-swap" title="Switch start and destination">Swap</button>
<label>To <input id="journeyToQuery" placeholder="Alexanderplatz" autocomplete="off" /></label>
<input id="journeyToStop" type="hidden" />
<div id="journeyToSuggestions" class="stop-suggestions"></div>
<label>Via <input id="journeyViaQuery" placeholder="optional stop" autocomplete="off" /></label>
<input id="journeyViaStop" type="hidden" />
<div id="journeyViaSuggestions" class="stop-suggestions"></div>
<div class="journey-mode" role="radiogroup" aria-label="Route mode">
<label><input type="radio" name="journeyMode" value="transit" checked /> Public transport</label>
<label><input type="radio" name="journeyMode" value="walk" /> Walk</label>
<label><input type="radio" name="journeyMode" value="drive" /> Car</label>
</div>
<div class="journey-options">
<label>Date <input id="journeyServiceDate" type="date" /></label>
<label>Departure <input id="journeyDeparture" type="time" value="08:00" /></label>
<label>Transfer buffer <input id="journeyTransferMinutes" type="number" min="0" max="60" step="1" value="2" /></label>
<label>Rank by
<select id="journeyRanking">
<option value="recommended">Recommended</option>
<option value="earliest_arrival">Earliest arrival</option>
<option value="duration">Shortest duration</option>
<option value="fewest_transfers">Fewest transfers</option>
</select>
</label>
</div>
<label class="journey-direct"><input id="journeyDirectOnly" type="checkbox" /> Direct public transport only</label>
<div class="journey-actions">
<button type="button" id="journeyEarlierBtn">Earlier</button>
<button type="submit" class="primary">Search</button>
<button type="button" id="journeyLaterBtn">Later</button>
</div>
<button type="button" id="generateItinerariesBtn">Generate travel options</button>
</form>
<div id="journeyResults" class="journey-results"></div>
<section class="itinerary-panel">
<div class="journey-title">
<span>Comparison</span>
<button type="button" id="reloadItinerariesBtn">Reload</button>
</div>
<div id="itineraryResults" class="itinerary-results muted">Generate travel options to compare route families.</div>
</section>
</section>
<div class="legend">
<span><b class="line osm"></b>OSM existing routes</span>
<span><b class="line gtfs"></b>GTFS covered routes</span>
<span><b class="line missing"></b>GTFS missing OSM match</span>
<span><b class="dot stops"></b>Stops / stations / terminals</span>
</div>
</section>
</main>
<div id="overlay" class="overlay" hidden>
<section class="overlay-panel">
<div class="overlay-title">
<h2 id="overlayTitle">Candidates</h2>
<button id="overlayCloseBtn">Close</button>
</div>
<div id="overlayContent"></div>
</section>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
<script src="/static/app.js?v=20260701-harmonizer-module"></script>
</body>
</html>