Files
meubility-workbench/app/templates/index.html
2026-07-01 23:29:51 +02:00

330 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>