- Saving a Segment, including an external provider route, clears its unsaved-changes navigation warning while preserving warnings for other unsaved editor drafts.
- Back to Trips and Save & Exit use a single custom confirmation when discarding unsaved drafts, without a second browser prompt. Reloading or closing the page still warns about unsaved changes.
- No database migration, API, or Mobile changes since v1.9.13. Older upgrades must still apply pending migrations; preserve PostgreSQL and its matching Data Protection key ring.
- Reload open Trip Editor pages after deployment. Follow the server-build deployment workflow using this tagged source.
- External route generation and proposal Save preserve inactive/custom Segment choices and nullable planning identity independently of the selected provider directions mode. Generate requests a preview without a replacement confirmation (#583 correction).
- Pending external routes now appear as a magenta dashed map preview with white casing above ordinary routes, temporarily dimming only the complete current Segment line, with labelled distance and travel-time estimates rounded only for display to at most two decimals. The current line retains 80% opacity and flat preview dash ends reveal overlapping geometry. Discard clears only the proposal; Save retains the validated #584 flow and Manual-duration override (#583).
- Segment Save now validates and atomically persists a pending external-route proposal with other Segment edits. Generate/preview leaves the draft unchanged, Discard preserves edits, and the separate Accept action/endpoint is removed. Validated estimates and provenance survive unchanged follow-up saves (#584).
- No database migration was added since v1.9.12. Upgrades from older versions must still apply their pending migrations. Preserve PostgreSQL and its matching Data Protection key ring.
- Route proposals now persist only through Save Segment; the separate acceptance endpoint is removed. Reload an already-open Trip Editor after upgrading. Segment planning labels remain independent of the explicitly selected provider directions mode.
- Follow the server-build deployment workflow using this tagged source.
- Suppressed Segment chevrons whose sampled direction contradicts local route traversal at tight returns across zoom levels, with matching Editor and Viewer behavior (#580).
- Thickened Segment chevron strokes to 3 px inactive / 4 px selected in the Trip Editor and Viewer, including print, while preserving geometry and placement (follow-up to #577).
- No database migration was added since v1.9.11; migration files and the model snapshot are unchanged. The latest migration remains
20260905095140_AddLocationProviderAddressLine1. Upgrades from older versions must still apply their pending migrations. Preserve PostgreSQL and the matching Data Protection key ring. - Follow the server-build deployment workflow using this release source.
- Widened Segment direction chevrons across the route line in the Trip Editor and Viewer for better visibility, preserving their length, spacing, and styling (#577).
- No database migration was added since v1.9.10; migration files and the model snapshot are unchanged. The latest migration remains
20260905095140_AddLocationProviderAddressLine1. Upgrades from older versions must still apply their pending migrations. Preserve PostgreSQL and the matching Data Protection key ring.
- Location statistics now share exact ASCII-trimmed, parent-scoped grouping and combine “East Macedonia and Thrace” with “Eastern Macedonia and Thrace” only under “Greece”. Parent scoping can increase counts; the region correction can decrease them. Both Timeline views show missing-parent sections and safely encode labels; tied visits select one deterministic settlement coordinate (#573). See Timeline statistics for sources and the remaining string-only ambiguity: identical names within identical parents cannot be distinguished, while other alternate labels may still split one entity. Stored values and released-Mobile API shapes remain unchanged; no migration or provider calls are added.
- Geoapify Location enrichment now stores street then house number, keeps settlement and state at their documented levels, and retains the independent provider line. Location maps, tables, timelines, groups and edit summaries prioritize structured addresses and show nearby feature metadata beneath them (#572).
- CSV history exports use an explicit-offset enrichment timestamp so valid retained provenance survives backend round trips (#572).
- Backend history imports preserve internal newlines and tabs in retained provider address lines; GPX/KML round trips normalize line endings to LF (#572).
-
Additive migration
20260905095140_AddLocationProviderAddressLine1adds a nullable 500-character Location field. Existing rows are not rewritten; repair remains fill-only. Mapbox mappings, Trip Place preference and released-MobileFullAddressremain compatible. -
Apply this migration when upgrading. It preserves existing rows and does not correct historical stored address values or mixed fields. Preserve PostgreSQL and its matching Data Protection key ring.
- Cancelled enrichment again offers explicit Retry and Repair actions. Prepared address repairs now appear in progress, resume automatically when transient backoff is due, and preserve replacement ownership after cancellation; locality-free outcomes are explained without automatic repeat contacts (#566).
- Location address edits preserve exact publication timestamps and original concurrency authority after validation errors, preventing false conflicts and stale overwrites. Restored the native Address details disclosure indicator (#565).
- Restored isolated PostgreSQL Quartz restart, recovery, and no-replay test coverage, including protection of unrelated workflow state and scheduler cleanup (#568).
- No database migration was added since v1.9.8; the migration files and model snapshot are unchanged. Preserve PostgreSQL and the matching Data Protection key ring when upgrading.
- These notes describe prepared release source, not a completed deployment. The original production enrichment inactivity has not been conclusively diagnosed.
- Location editing now supports manual correction of all eight address fields in a responsive expandable section, with custom Wayfarer feedback and protection against concurrent provider updates (#560).
- Added a link to the Wayfarer GitHub project in the shared footer (#560).
- Incomplete Geoapify-enriched Locations can now be explicitly repaired without overwriting existing address values; the import workflow reports these rows separately from permanently deferred failures (#559).
- Missing-address enrichment distinguishes rows available for explicit retry from invalid-coordinate rows, reports the accepted retry count, and explains when no rows remain eligible (#550).
- Map and Trip popups retain meaningful detected-place names while replacing technical reverse-geocoding feature types with plain-language address-precision guidance for broad results (#551).
- Missing-address enrichment shows intermediate committed progress through its authenticated SSE refresh channel (#554).
- Detailed country, region, and city statistics tolerate partial address hierarchies on both Timeline views and report failures through Wayfarer's custom alert surface (#557).
- Replaced the repository LOC checker with pinned Agent Code Guard, preserving current legacy file allowances through a non-increasing ratchet and checking source and Markdown in CI (#523).
- Made the PostgreSQL location-index regression test deterministic (#542).
- No new database migration is introduced relative to 1.9.7. Preserve the PostgreSQL database and matching Data Protection key ring when upgrading.
- Manual changes to address fields clear provider attribution; unchanged addresses retain it. Changing coordinates clears old address fields unless explicitly changed in the same save.
- Restored durable Geoapify geocoding and directions provider selection, corrected validation placement, and accepted bounded provider snapping and documented routing response semantics in verification, Trip routing, and Mobile routing.
- Geoapify Directions now accepts the provider's documented JSON
{lon, lat}route geometry. Configured providers remain visible, and credential changes retain saved geocoding and directions choices while safely requiring reverification (#547).
- Personal provider setup at
/User/LocationProviderSettingsnow provides independent Geocoding and Directions workflows with explicit verification and provider selection. A credential alone does not authorize contact, each capability is verified and selected independently, and no administrator routing template is required (#538 step 1). - Geoapify Directions now requires an explicit provider-native mode. Transport Profiles remain independent manual-planning provenance and do not infer provider modes; released Mobile clients remain compatible only through the bounded exact stable-key adapter for omitted modes (#538 step 1).
- Mapbox Permanent Geocoding remains supported under its existing consent and provider-policy contract. Mapbox Directions remains unsupported (#538).
- Production source deployment now selects
Wayfarer.Models.ApplicationDbContextexplicitly when applying EF migrations in a repository with multiple contexts (#535).
- Geoapify capability verification now uses meaningful fixed public probes, production response parsing, bounded known- and unknown-length reads, and safe actionable outcomes. Displayed usage is truthful local Wayfarer-admitted usage, not provider-confirmed billing (#537).
- Retired legacy administrator routing providers, templates, Transport Profile mappings, global routing authority, duplicate user routing credentials, generic OSRM execution,
/User/RoutingSettings,/User/ApiToken, and their administrative and user interfaces (#538 step 3). - The retirement migration deletes obsolete routing ciphertext and configuration while preserving accepted Segment route provenance, saved geometry, and independent Transport Profiles (#538 step 3).
- Before upgrading, back up PostgreSQL and the matching complete Data Protection key ring. Apply migrations before starting the new scheduler/application.
- Rollback across the routing-authority retirement migration requires restoration of the compatible pre-upgrade database and its matching key ring; deploying older binaries alone cannot reconstruct deleted credentials, templates, mappings, selections, or global configuration.
- Added protected per-user location-provider profiles with explicit credential verification, independent geocoding and routing selection, revocation, provider-neutral status, and shared bounded usage admission. Legacy Mapbox credentials migrate without exposing or deleting them, but remain inactive until explicitly authorized and selected (#499).
- Added explicitly authorized Mapbox Permanent Geocoding for persistent Location and Place enrichment. Consent is bound to the verified credential generation, accepted results retain provider and storage provenance, and incomplete or changed authority fails closed without blocking tracking, synchronization, imports, Trips, or existing enrichment (#501).
- Added personal Geoapify reverse geocoding and persistent enrichment, plus provider-neutral routed geometry with stable profile mappings, accepted-route provenance, Geoapify/OpenStreetMap attribution, and storage-authorized offline reuse. Geocoding and routing share one guarded per-user allowance, and failed or invalid responses cannot replace retained accepted data (#502).
- Added durable Quartz-backed enrichment workflows for large location-history imports, with bounded batches, Start/Pause/Resume/Cancel/Retry controls, allowance-aware wake-up, authenticated progress, restart reconciliation, and privacy-safe diagnostics. Import completion remains separate from explicitly opted-in enrichment (#507).
- Added explicit-submit Trip Editor place search through the active personal Geoapify authority. Provider contact consumes the shared allowance, authority-bound cache reuse is free, and visibly attributed public Nominatim fallback is limited to authorized no-selection, Mapbox-selection, or exhausted-Geoapify cases; invalid active Geoapify authority fails closed (#526).
- Added optional provider-returned named feature metadata for reverse-geocoded Locations and Trip Places, preserving authenticated provenance and supported import, export, clone, API, and presentation round trips. It remains separate from retained addresses and user-authored labels and does not perform nearby-place discovery (#518).
- Added authenticated, provider-neutral Mobile routing-profile discovery with catalog identity through chooser and capability confirmation, selected-profile execution authority, and separate current Segment profile identity. Discovery contacts no provider, consumes no credit, and remains compatible with released Mobile clients; Mobile-side retained-route ownership is released separately (#528).
- Location-history imports now stream and persist bounded batches for Google Timeline JSON, Wayfarer GeoJSON, CSV, GPX, and location-history KML, with durable replay/deduplication, restart recovery, deletion fencing, opaque staging names, and bounded diagnostics. Unsupported generic GeoJSON is rejected, while Trip import remains a separate workflow (#507).
- Geoapify routing now parses documented per-leg geometry, translates step indices across flattened legs, preserves structural waypoint provenance, rejects oversized or invalid anchor sets before usage admission or provider contact, and rejects malformed provider responses before proposal acceptance or persistence (#517).
- New additive migrations introduce protected provider profiles and usage state, Mapbox consent/provenance, Geoapify accepted-route provenance, durable enrichment/import lifecycle authority, optional feature metadata, and concurrency-safe compatibility-profile creation. Deployments must preserve and restore PostgreSQL together with the matching Data Protection key ring and apply the ordered migrations before scheduler startup; production deployment and migration remain pending (#499, #501, #502, #507, #518, #505).
- Provider contact, persistent storage, billing admission, and attribution are explicit authority boundaries: credentials alone grant no contact or storage permission; admitted external contacts remain charged after timeout or failure; Geoapify/OpenStreetMap and Nominatim attribution remains visible where their results are used (#499, #501, #502, #526).
- Finalized durable provider, import/enrichment, Mobile-routing, migration, deployment, backup/restore, and rollback guidance for the backend-first rollout. GitHub release publication, deployment, production migration, and the released-Mobile compatibility smoke remain pending separate authorization (#505).
- Corrected protected group invitation and membership notifications to use authenticated per-user streams with content-free reload hints, authoritative revocation, bounded reloads, and no caller-selected or cross-user channel disclosure (#514).
- Corrected concurrent same-key compatibility transport-profile creation so Segment inserts reuse one deterministic database identity, while deterministic UUID collisions belonging to a different normalized key remain rejected (#505).
- Isolated personal provider credentials behind the retained Data Protection authority, masked them across user and administrative surfaces, removed query/key-bearing provider URI logging, and required explicit current authority before provider contact or publication (#499, #501, #502, #526, #528).
- Hardened import, enrichment, routing, and notification diagnostics and progress channels so credentials, raw provider responses, coordinate-bearing URLs, personal search text, staging names, and cross-user state are not disclosed (#507, #514).
- Corrected the guarded PostgreSQL clone-workflow fixture to reuse its persisted API user instead of attempting a duplicate identity insertion, restoring deterministic relational release validation (#495, #497).
- Added authenticated per-user idempotency-key handling to CSV and Wayfarer GeoJSON location imports so mobile offline-queue recovery imports and normal location delivery converge on one server record while retaining legacy keyless import behavior (#244, #494).
- Aligned the opt-in PostgreSQL KML import fixtures with the v1.9 generic/native classification contract while retaining focused rollback, upsert, conflict, concurrency, and recovery protection (#487, #491).
- Added themed hover tooltips to active Segment route badges so A/B/C anchors identify their Start/Via/End role and Place name consistently in the Trip Editor and Viewer (#489, #490).
- Added ordered saved-place waypoints within a single trip segment, including accessible web authoring, all-anchor fallback routes, anchor-aware custom geometry, readable/viewer presentation, clone support, and backward-compatible public API exposure (#388, #403–#414).
- Added Wayfarer-native KML schema v2 round trips for waypoint identity, route indices, transport profiles, measurements, and Automatic/Manual provenance while retaining native v1 compatibility and exact waypoint-free generic KML behavior (#413, #414).
- Added active Segment route presentation across the Trip Editor and Viewer, including Start/Via/End trails, direction cues, route-order badges, synchronized selection, draft-only route reversal, and readable/print parity (#389, #442).
- Added external route generation for administrator-approved providers, with configurable provider pacing and optional personal credentials that remain isolated from server-owned secrets (#426, #448, #449, #451, #453, #454).
- Replaced the fixed runtime transport-mode catalog with administrator-managed database profiles and made saved route geometry plus explicit measurement provenance authoritative for segment estimates (#403, #405).
- Updated vulnerable and supported frontend/backend dependencies and development toolchains, including Vite 8, stable TypeScript 6 typechecking, aligned Playwright packages, Swashbuckle 10, and .NET 10-compatible test tooling (#421, #465–#475).
- Added fixed vertex, fidelity, and processing budgets for oversized generic KML routes before persistence, with exact endpoint preservation and bounded simplification reporting during import (#425, #444).
- Aligned fresh and existing PostgreSQL Quartz schemas with the pinned Quartz 3.19 contract under transactional, serialized startup validation (#478, #484).
- Corrected project documentation by removing unsupported MBTiles and offline mobile-map claims.
- Fixed Trip Editor rich notes so ordered and bullet lists no longer recreate or persist empty terminal items, while canonical spacing remains consistent across editor, viewer, readable, print, and PDF surfaces (#433, #435).
- Fixed Trip Editor Region disclosure controls so Regions required by active editor, selection, or search context clearly remain expanded without mutating the user's saved collapse choices, including after reorder recovery (#432, #436).
- Gave the Trip Editor map one stable accessible identity with accurate default, Place, Area, and Segment map-work descriptions (#437, #439).
- Contained the global navbar at browser-zoom widths by using the native collapsed navigation state while preserving keyboard access and long account/menu content (#441, #443).
- Bounded decoded dimensions, frame counts, and memory estimates for optimized public image-proxy inputs while retaining still-image compatibility (#476, #477).
- Corrected Segment route chevron ownership, sizing, contrast, and route-badge layout after zoom and visibility changes (#479, #482).
- Preserved custom route geometry when adding or removing intermediate saved places instead of replacing the route with straight lines (#481, #483).
- Stopped treating generic KML route titles as transport authority; imported routes remain unassigned and show one post-import reminder to select transport modes where needed (#480, #486).
- Restored generated trip map thumbnails on deployments using restrictive public
AllowedHostsby keeping browser capture on loopback with an authorized Host, rejecting failed navigation, and publishing completed JPEGs atomically (#401).
- Added explicit Interactive, Conservative, and Custom/Provider Agreement tile traffic modes. Interactive removes Wayfarer-originated rate-token and per-client cold-series throttling for ordinary human-driven map use while retaining bounded concurrency, queues, caching, coalescing, retries, and provider-directed safety gates (#396, #397).
- Increased Interactive map responsiveness to the existing 12-request global and six-request per-client scheduler bounds, while Conservative mode provides explicit 12-contact/second, burst-40, concurrency-eight, and 480-series/client/minute safeguards (#396, #397).
- Removed the incompatible Thunderforest and CARTO presets and fail closed for their known endpoints without deleting preserved configuration or provider-scoped cache data. Corrected the OpenTopoMap preset attribution and documented exact single- and multi-host
AllowedHostsdeployment configuration (#396, #397). - Existing supported built-ins migrate deterministically to Interactive mode and compatible Custom settings remain preserved. Traffic-mode changes do not change cache identity and require no cache purge (#396, #397).
- Corrected manual Trip Editor place positioning so Add Place preserves the current high-zoom viewport, initializes one authoritative marker at the map center, and retains click, drag, styling, Done, Cancel, Reset, Save, and responsive mobile behavior without duplicate markers (#386, #398).
- Kept edited segment routes visible after map-work Done and before Save, with deterministic persisted/draft/work ownership, failure-safe lifecycle handling, distinguishable route states, and contained docked/mobile segment controls (#387, #399).
- Added provider-aware tile policies with distinct built-in profiles, bounded custom-provider controls, HTTP/2 preference with HTTP/1.1 fallback, and an explicit Admin notice for deployments retaining the historical 30-per-minute cold-miss allowance (#385, #394).
- Increased Wayfarer's default interactive provider profile to 6 sustained requests per second, burst capacity 20, and concurrency 6 while retaining bounded queues, per-client protection, provider-directed backoff, caching, cancellation, and the prohibition on OSM prefetch or offline downloads (#385, #393, #394).
- Prioritized visible cold tiles over stale background refresh, coalesced duplicate cold misses, isolated cache and in-flight work by provider identity, and made cold viewports fill progressively instead of entering synchronized 503 retry waves (#385, #393).
- Corrected upstream retry and status handling so every real contact consumes provider capacity, permanent responses are not retried, transient failures remain transient, and provider
Retry-Afterinstructions are honored without cancellation or privacy regressions (#385, #391). - Unified sanitized provider attribution across the Trip Editor, authenticated/public/embedded Trip Viewer maps, readable snapshots, and PDF output, including the required OpenStreetMap copyright link without mislabeling custom providers (#385, #392).
- Added deterministic route numbering for regions and places in the Trip Editor, normal trip viewer, readable view, and readable browser print output. Reordering updates and saves the numbering automatically, while raw names remain unchanged.
- The visible Unassigned Places region is fixed at
0, and place numbering restarts from1within every region.
- Restored the public Trip Viewer map across phone, tablet, and desktop layouts, contained the phone sidebar within the viewport, and kept the trip area centered through sidebar collapse and expansion (#379)
- Reflected the previously accepted live-sharing confirmation in timeline settings so unrelated changes, such as the timeline title, can be saved normally (#377)
- Added an optional custom timeline heading in user settings, with a clear fallback to the existing display-name heading (#356)
- Standardized the shared footer's responsive layout and link styling across standard pages (#365)
- Improved phone containment for account settings, API tokens, location imports, and user/manager group tables and maps (#370, #371, #372)
- Updated Vite to 7.3.6 and transitive esbuild to 0.28.1 to resolve Windows development-server security advisories (#360)
- Reconciled global tags safely during trip imports and replaced raw import-error output with safe user-facing feedback (#354)
- Made public timeline sharing fail closed for invalid thresholds and stopped active SSE streams after eligibility is revoked (#363)
- Moved the shared footer version next to the copyright year and made the year use UTC so deployed pages show the current year without duplicating
Wayfarerat the end of the footer (#330)
- Added a single compiled Wayfarer runtime version source backed by
Version.props, exposed through the app CLI,/api/version,X-Wayfarer-Version, and the shared footer display (#322, #325) - Added repo-local release helper automation for deterministic version bump preparation, changelog skeleton insertion, offline release checks, explicit local tag checks, and explicit GitHub release validation (#324, #326)
- Added app CLI help output for
help,version --help, and reset-password usage without starting the web host or database-backed command path (#327, #328)
- PDF trip export cover snapshots now use the shared image proxy/cache pipeline instead of direct raw cover-image downloads, preserving proxy validation, cache reuse, and origin-work coordination (#319, #323)
- Map tiles from the local cache now render immediately when stale while bounded background refresh revalidates them with the provider, preserving OSM-safe conditional requests and avoiding request-path waits on cached map views (#316, #318)
- Proxied trip and region images now serve stale optimized local cache files immediately while refresh and cache-miss work is coalesced per image and protected by a process-wide origin/ImageSharp budget (#317, #320)
- Local proxied-image cache hits now bypass anonymous image-proxy rate limiting, while origin downloads and optimization work remain protected for both anonymous and authenticated users (#317, #320)
- Fixed repeated slow tile reloads for cached map areas by moving expired tile revalidation off the response path, adding bounded retry/backoff, atomic tile replacement, and Trip Editor retry/concurrency parity (#316, #318)
- Fixed slow repeated proxied image loads for cached trip images by keeping expired local files usable, refreshing them in bounded background work, and preserving old bytes/metadata on refresh failures (#317, #320)
- Fixed raw cover fallback render paths for public trip thumbnails, quick views, and legacy trip/region cover views so they use the local image proxy instead of direct external image URLs (#317, #320)
- Trip export snapshot cover downloads still use the raw cover URL and are tracked separately for a later release (#319)
- Improved the Trip Editor visit progress/history modal with region progress bars, clearer filter controls, status icons, visit-count pills, and compact first/last/history rows (#309, #311)
- Added a phone-only Trip Editor map-first bottom drawer with
Trip,Regions, andSegmentstabs, deterministic drawer states, mobile search placement, dirty-editor guards, and protected desktop/tablet breakpoints (#312, #313, #314) - The
reset-passwordadmin CLI now runs as a scoped command host and exits after completion instead of continuing into normal web app startup (#306, #307)
- Fixed Trip Editor public/progress URLs so editor-generated links use the canonical public trip route instead of
/Public/TripViewer/View/{id}(#308, #310) - Fixed the
reset-passwordCLI service setup to avoid the ASP0000BuildServiceProviderwarning while preserving normal Identity password reset behavior (#306, #307) - Fixed Trip Editor mobile drawer polish issues found during published-bundle validation, including transparent sticky edit headers/footers, non-draggable handle affordance, inconsistent drawer controls/heights, and desktop-to-phone Trip edit/view resize behavior (#314, #313)
- Replaced the legacy Trip Edit experience with the Vue/Vite Trip Editor on the canonical
/User/Trip/Edit/{id}route (#236, #238, #240, #242, #244, #246, #252, #258, #259, #263, #264, #265, #268, #271, #278, #280, #281, #282, #283) - Added Trip Editor support for metadata, regions, places, areas, segments, tags, share-progress settings, visit progress/history, rich notes, geosearch add-place, coordinate picking, map navigation, map utilities, and shared docked/expanded editor surfaces (#238, #240, #242, #246, #252, #258, #259, #263, #264, #265, #268, #271, #278, #280)
- Added searchable icon and marker-color selectors, selected-place map/sidebar/status synchronization, popup/marker parity, and responsive light/dark Trip Editor polish (#288, #289, #290, #291, #292, #294)
- Added real endpoint Trip Editor contract coverage for CRUD persistence, search-add persistence, rich-notes persistence, stale/error/dirty/delete feedback, and Development/published asset smoke checks (#297, #298, #299, #300, #301, #302, #303, #304)
GET /User/Trip/Edit/{id}now serves the Vue Trip Editor directly; the old/User/Trip/Workspace/{id}route and legacy editor fallback were removed during cutover cleanup (#282, #283)- Trip Editor rich notes are normalized at the Trip Editor request boundary before persistence, including canonical image URL handling, allowed Quill formatting/alignment, unsafe attribute stripping, and trailing helper paragraph cleanup (#302)
- Deployment now requires the Trip Editor Vite assets to be built before
dotnet publish; server-build deployments need Node.js/npm build tooling and must generatewwwroot/vite/trip-editor/manifest.json(#287, #303) - Development mode uses the Vite dev server assets only, while published/non-Development mode loads manifest-based Trip Editor bundle assets (#303)
- Trip Editor Playwright coverage now distinguishes real endpoint contract proof from mocked UI/request-shape/visual tests (#297, #298)
- Fixed published-output Trip Editor asset loading by moving the Vite manifest to the publish-safe
wwwroot/vite/trip-editor/manifest.jsonpath (#287) - Fixed ASP.NET static asset serving/compression interactions for generated
/distand/vite/trip-editorbundle outputs (#285) - Fixed source-tree Production-run documentation by documenting published-output acceptance as the supported production-like local test path (#286)
- Fixed Trip Editor release-candidate parity regressions found during manual validation, including Unassigned Places behavior, geosearch add-place defaults and persistence, marker/icon rendering, map search result containment, coordinate pick behavior, copy-link feedback, and selected editor/sidebar state coherence (#288, #289, #290, #291, #292, #294, #296)
- Fixed final Trip Editor E2E nondeterminism around copy-link feedback timing, inline segment editor layout overlap, sidebar search fixtures, visual-polish expand targeting, real CRUD cleanup, and rich-notes image assertions (#304)
TileMetadataHotCacheSizeMBapplication setting with Admin Settings UI support and client-side derived entry-count hint for the zoom>= 9tile metadata hot cache (#217)
- Warm zoom
>= 9tile hits now use an in-process metadata hot cache to avoid the per-hit Postgres metadata read on the common fresh-cache path (#217) - Hot metadata cache invalidation now participates in purge, eviction, and tile-delete paths while preserving DB/file durability ordering and existing revalidation behavior (#217)
- Fresh hot-hit
LastAccessedthrottling is now atomic per tile and retries immediately after failed DB persists instead of suppressing writes for the full cooldown window (#217)
- HIGH: LRU/full cache purge timed out on large caches (~500MB), showing error page despite successful deletion. Purge now runs in background with immediate HTTP 202 response (#207)
- Cache lock contention during purge blocked concurrent tile writes. Reduced file-delete chunk size from 100 to 10 with
Task.Yield()between chunks to prevent writer starvation (#207)
- SSE-based real-time progress reporting for cache purge operations — admin UI shows animated progress bar with file count and percentage (#207)
- Atomic purge-in-progress guard (
Interlocked.CompareExchange) prevents concurrent purge operations; second request returns 409 Conflict (#207) TileCachePurgeSseendpoint for SSE subscription andTileCachePurgeStatusendpoint for on-load reconnect (#207)- On page load, admin settings UI checks purge status and reconnects SSE if a purge is mid-flight (#207)
- Tile-provider-change purge now respects the concurrency guard — skips gracefully if manual purge is running (#207)
DeleteAllMapTileCacheandDeleteLruCacheendpoints return HTTP 202 Accepted (was 200 with awaited result) (#207)PurgeAllCacheAsyncandPurgeLRUCacheAsyncaccept optionalSseService/channel params for progress broadcasting (#207)
- Outbound budget burst capacity raised from 10 to 12 — allows 2 more tiles through on initial burst before settling into sustained 2/sec rate, reducing 503s on cold-cache loads (#214)
- Outbound budget acquire timeout raised from 3.0s to 3.5s — extra 0.5s yields 1 more token from replenishment per wave, reducing false timeouts (#214)
- Client concurrency pool multiplier raised from 60% to 75% of burst capacity (pool size 6 → 9) — more tiles queue server-side instead of waiting client-side (#214)
- Budget retry-after interval updated from 5s to 6s to align with new burst refill time (12 tokens / 2 per sec = 6s) (#214)
- Client slow-retry interval auto-derives to 18s (was 15s) from updated retry-after × 3 (#214)
- HIGH: Slow retry phase replayed full 5-attempt fast-retry cycle on each poll (~34s lag per attempt, 6 per-IP budget hits per cycle). Now makes single-shot fetches — one request per slow poll (~15s intervals), one budget hit each (#206)
_scheduleSlowRetrynow calls_slowRetryOnce(single fetch + reschedule on 503) instead of resetting to_fetchWithRetryattempt 0 (#206)
_slowRetryOncemethod — lightweight single-fetch slow-phase handler that avoids the overhead of the full fast-retry state machine (#206)
- Default per-IP outbound budget increased from 30 to 80 cache misses/min — 30 was too low for cold-cache zoom-17 loads (~35 tiles), causing immediate per-IP rejection before retries could succeed (#206)
- Slow retry interval reduced from 30s to 15s — derived from server's
retryAfterSeconds * 3instead of hardcoded (#206) - Client-side concurrency pool size now derived from server's
burstCapacity * 0.6(injected viawayfarerTileConfig) instead of hardcoded 6 (#206) - Slow retry delay now derived from server's
retryAfterSeconds * 3(injected viawayfarerTileConfig) instead of hardcoded 30s (#206) TilesController.BudgetRetryAfterSecondschanged fromprivate stringtointernal intfor config injection (#206)TileCacheService.OutboundBurstCapacityadded as public accessor forOutboundBudget.BurstCapacity(#206)wayfarerTileConfigin_Layout.cshtmlnow includesburstCapacityandretryAfterSecondsfrom server config (#206)
- MEDIUM: Tiles that exhausted fast retries on 503 went permanently gray with no recovery path — after the per-IP budget window decayed, those tiles could have loaded but never retried again (#206)
- Slow retry phase in
retryTileLayer.js— after 5 fast retries exhaust on 503 or network error, tiles enter indefinite 30-second polling (with ±25% jitter) until they load or are removed; ensures all tiles eventually appear even when per-IP budget temporarily blocks them (#206) _scheduleSlowRetrymethod andslowRetryDelayMsoption (default 30s) onRetryTileLayer(#206)
- HIGH: Cascading 503 on cold-cache tile loading — per-IP outbound budget counter incremented on every request (including those rejected by the global budget), so client-side retries found the counter already past the limit and failed immediately, causing all tiles to gray out permanently (#206)
- Client-side concurrency pool (6 slots) in
retryTileLayer.js— tiles queue client-side and stream in progressively instead of blasting ~35 simultaneous requests that overwhelm server budgets (#206) - Two-phase per-IP rate limiting:
WouldExceedRateLimit(peek without increment) andRecordRateLimitHit(increment only) inRateLimitHelper— enables check-then-record pattern where only actual upstream fetches count against the per-IP limit (#206) PeekCountmethod onRateLimitEntry— read-only weighted sliding-window count for speculative checks (#206)
SendTileRequestCoreAsyncnow uses two-phase per-IP budget: peeks first (fast-fail), then records the hit only after global budget token is acquired (#206)
RetryTileLayer— custom Leaflet TileLayer subclass usingfetch()for HTTP status code access; retries on 503 with exponential backoff andRetry-Afterheader support (#206)createTileLayer()factory inretryTileLayer.js— centralizes tile layer creation, replacing duplicated boilerplate across 13 JS files (#206)TileRetrievalResult— typed result class distinguishing tile success, not-found, and budget-throttled states (#206)RequestIdLoggingMiddleware— pushesHttpContext.TraceIdentifierinto SerilogLogContextso every log entry includesRequestIdautomatically (#206)- Serilog
.Enrich.FromLogContext()and{Properties:j}output templates for console and file sinks (#206) DbMetadataZoomThresholdconstant replacing magic number9acrossTileCacheService(#206)- Inline
tileerrorretry fallback for HiddenAreas Create/Edit views (cshtml inline scripts) (#206)
- HIGH: Cold-cache tile loading returned 404 for budget-exhausted tiles — Leaflet treated as permanent failure, showing persistent gray areas. Now returns 503 +
Retry-Afterheader; client retries automatically (#206) - MEDIUM: Ghost metadata rows stored in DB when tile fetch was aborted by budget exhaustion — rows had
Size=0, nullETag/ExpiresAtUtc, pointing to non-existent files (#206) - MEDIUM: Potential blob URL memory leak in
RetryTileLayer— if a tile was removed (panned/zoomed away) while blob was being read, the revoke callback never fired; now guarded withsignal.abortedcheck (#208) - LOW: Client-side retry thundering herd — all 503'd tiles retried at identical intervals; now adds ±25% jitter to backoff delays (#208)
- LOW: Zero or negative
Retry-Afterheader values caused immediate retry; now clamped to base delay floor (#208) - LOW:
CacheTileAsyncupstream failure path relied on downstream null guard; now returns early with explicit intent (#208)
CacheTileAsyncnow returnsbool(false= budget exhaustion) instead ofvoid(#206)RetrieveTileAsyncnow returnsTileRetrievalResultinstead ofbyte[]?(#206)PerformanceMonitoringMiddlewarelog line now includes explicitRequestIdparameter (#206)BudgetRetryAfterSecondsconstant extracted with doc linking toOutboundBudgetconfig (#208)ReadAsByteArrayAsyncand inter-retryTask.Delaynow passCancellationTokenfor prompt cancellation (#208)TileCacheServiceTestsandTilesControllerTestsnow share[Collection("OutboundBudget")]to prevent parallel test interference (#208)
- Per-IP outbound budget tracking (default 30 cache misses/min/IP) — prevents a single client from monopolizing the global outbound token budget (#204)
- Admin UI field for configuring per-IP outbound budget limit (0 = disabled) (#204)
- HIGH: Outbound budget starvation DoS — a single attacker could exhaust all outbound tokens with uncached tile requests, denying service to legitimate users (#204)
- HIGH: IPv4-mapped IPv6 addresses not normalized on direct-IP path —
::ffff:x.x.x.xandx.x.x.xcreated separate rate-limit buckets, bypassing limits (#204) - HIGH: Concurrent insert race in
CacheTileAsync— two requests for the same uncached tile could trigger an unhandledDbUpdateExceptionfrom the unique index; now caught as a benign race (#204) - MEDIUM:
PurgeBatchAsyncandPurgeLRUCacheAsyncdecremented_currentCacheSizeusing stale projected sizes instead of re-fetched entity sizes, causing cache size drift (#204) - MEDIUM:
PurgeAllCacheAsyncloaded full entities into memory for the metadata dictionary; now projects onlyIdandTileFilePathwithAsNoTrackingto reduce memory usage on large caches (#204) - MEDIUM:
RetryOperationAsynccaught all exceptions including non-transient ones; now catches onlyDbUpdateExceptionso non-recoverable errors propagate immediately (#204) - MEDIUM-LOW: Revalidation coalescing captured first caller's
CancellationToken— client disconnect cancelled outbound request for all coalesced waiters (#204)
- Unique composite index on
TileCacheMetadata(Zoom, X, Y)— eliminates sequential scans on every tile request (#204) - Periodic tile cache size reconciliation via
RateLimitCleanupJob— corrects_currentCacheSizedrift from non-atomic updates every 5 minutes (#204) - Hard cap (50K entries) on rate limit caches with oldest-entry eviction — prevents unbounded memory growth from sustained low-rate attacks (#204)
CancellationTokenpropagation through tile request chain — requests abort when client disconnects instead of blocking threads (#204)
- Outbound budget
AcquireTimeoutreduced from 10s to 3s to prevent thread pool starvation under sustained cold-cache load (#204) PurgeAllCacheAsyncloads all DB metadata in a single query instead of O(N) individual queries per file (#204)PurgeLRUCacheAsyncnow deletes in chunks of 1000 IDs to prevent PostgreSQL query plan explosion from large IN clauses (#204)- Eviction and purge file deletion consolidated into single lock acquisition per batch, eliminating convoy effects (#204)
X-Forwarded-ForIP addresses normalized to canonical form — prevents IPv4/IPv6 aliasing from creating separate rate limit buckets (#204)- Eviction
_currentCacheSizedecrement now uses re-fetched entity sizes instead of stale projected sizes (#204)
- CRITICAL: Missing database index on hot-path tile lookup queries (
Zoom, X, Y) — every tile request was a sequential scan (#204) - CRITICAL:
PurgeAllCacheAsyncissued individual DB query per cached file — 100K files caused 100K sequential-scan queries (#204) - HIGH:
_currentCacheSizedrift from eviction using pre-fetched sizes instead of actual deleted sizes (#204) - HIGH: Thread pool starvation risk from 10-second outbound budget timeout under cold-cache load (#204)
- HIGH: Lock convoy during eviction/purge — per-file lock acquisition serialized all concurrent writes (#204)
- MEDIUM: Sliding-window rate limiter documentation understated worst-case jitter (up to full prevCount, not ~0.5) (#204)
- Sliding-window rate limiter replacing fixed-window — prevents boundary-batching attacks where bursts at window edges could double the effective limit (#204)
- Authenticated user rate limiting by user ID (default 2000 req/min) — previously authenticated users bypassed rate limiting entirely (#204)
TileRateLimitAuthenticatedPerMinuteapplication setting for configurable authenticated tile rate limit, exposed in Admin Settings UI (#204)- Outbound request budget (token-bucket at 2 req/sec, burst 10) — prevents cache-miss cascading from overwhelming upstream OSM and risking a fair-use block; complies with OSM 2-connection policy via transport-level enforcement (#204)
X-Content-Type-Options: nosniffheader on tile proxy responses to prevent MIME-sniffing (#204)
- Rate limiter now uses sliding-window counter approximation instead of fixed-window, smoothing request counting across window boundaries (#204)
- Default anonymous tile rate limit increased from 500 to 600 req/min to compensate for the stricter sliding-window algorithm (#204)
- Rate limiting applies to both anonymous (by IP) and authenticated (by user ID) requests with separate configurable thresholds (#204)
- Admin Settings UI updated to show both anonymous and authenticated rate limit fields; removed incorrect "never limited" text (#204)
- Outbound tile requests gracefully degrade (serve stale cache) when upstream budget is exhausted (#204)
- Rate limit cleanup flag is now per-cache instance instead of a shared global flag, allowing independent cleanup of anonymous, authenticated, and image proxy caches (#204)
X-Forwarded-Forheader values are now validated withIPAddress.TryParsebefore use as rate limit keys (#204)- Outbound budget
StopReplenishernow cancels the old CTS before creating replacements, eliminating brief replenisher overlap (#204)
RateLimitCleanupJob— periodic Quartz job (every 5 minutes) sweeps expired entries from all in-memory rate limit caches, preventing unbounded memory growth (#204)- Log warning when authenticated user lacks
NameIdentifierclaim and falls back to IP-based rate limiting (#204)
- Eviction
_currentCacheSizetracking now decrements after successful DB commit, preventing permanent undercount on failed eviction (#204) - Tile cache eviction now commits DB deletions before deleting files — previously files were deleted first, leaving orphaned DB records pointing to missing files if
SaveChangesAsyncfailed (#204) - Admin settings checkbox hidden-field fallback for
TileRateLimitEnabledandIsRegistrationOpen— unchecking now correctly postsfalseinstead of falling back to C# default (#204) - CRITICAL: Remove global read-lock on tile cache — file reads no longer serialize through
_cacheLock, eliminating a throughput bottleneck under concurrent map viewers. Writes and deletes retain the exclusive lock; reads catchIOExceptionas cache miss (#204) - CRITICAL: Increase outbound budget burst capacity from 2 to 10, reducing cold-cache map load times. OSM's 2-connection policy is now enforced at the transport layer via
SocketsHttpHandler.MaxConnectionsPerServer(#204) - HIGH: Eviction coalescing — concurrent
CacheTileAsynccalls can no longer trigger simultaneous eviction runs (double-evict). UsesInterlocked.CompareExchangeguard withDbUpdateConcurrencyExceptionhandling (#204) - HIGH:
EvictDbTilesAsyncnow uses a dedicatedIServiceScopeinstead of the per-request_dbContext, preventing disposed-context failures when eviction outlives the originating request (#204) - HIGH:
CacheTileAsyncno longer retries on outbound budget exhaustion — breaks immediately instead of blocking up to 30 seconds (3 retries × 10s timeout) (#204) - MEDIUM: Admin settings cross-field validation: authenticated rate limit must be >= anonymous rate limit (#204)
- Conditional requests (ETag / If-Modified-Since) for tile cache re-validation — expired tiles send conditional headers to upstream, serving cached data on 304 Not Modified (#201)
- Cache header compliance — parse and honour
Cache-Control: max-ageandExpiresheaders from upstream tile servers, with 7-day default fallback per OSM policy (#201) - Per-tile request coalescing — concurrent requests for the same expired tile are coalesced into a single upstream HTTP request (#201)
- In-memory sidecar metadata cache for zoom 0-8 tiles, eliminating disk I/O on the hot path (#201)
- Sidecar
.metaJSON files alongside zoom 0-8 tiles to persist ETag/Last-Modified/expiry across restarts (#201) ETag,LastModifiedUpstream, andExpiresAtUtccolumns onTileCacheMetadatafor zoom >= 9 tiles (#201)
- Use canonical OSM tile URL
https://tile.openstreetmap.org/instead of non-canonicalhttps://a.tile.openstreetmap.org/(#201) - Enforce minimum tile cache size of 256 MB in Admin Settings (OSM requires at least 7 days of cached tiles) (#201)
- Throttle
LastAccessedDB updates to once per 5 minutes per tile, reducing DB writes by ~99% for popular tiles (#201) - Graceful degradation: serve stale cached tiles when upstream re-validation fails (#201)
MaxCacheTileSizeInMB = -1(disable cache limit) now correctly skips LRU eviction instead of silently defaulting to 1024 MB (#201)
- Single DB round-trip for metadata load + conditional LastAccessed update on zoom >= 9 hot path (#201)
- Request coalescing reduces outbound HTTP requests under concurrent load (#201)
- 304 Not Modified responses avoid re-downloading unchanged tile content (#201)
- Fix OSM tile 403 "Referrer is required" by adding per-request Referer header and honest User-Agent to outbound tile proxy requests (#199)
- Move HttpClient header configuration from TileCacheService constructor to AddHttpClient DI registration for correct lifecycle management
- Remove redundant AddScoped registration (AddHttpClient already registers scoped)
- Use TryParseAdd for User-Agent with fallback when Application:ContactEmail contains invalid RFC 7230 characters
- Update TilesController IsValidReferer doc to clarify it is an abuse deterrent, not a security boundary
Application:ContactEmailconfiguration setting for tile provider User-Agent compliance (configurable via systemd env varApplication__ContactEmail)- Startup warning when ContactEmail is not configured in non-Development environments
- Deployment template and install.sh support for the new ContactEmail setting
- Server-side pagination for the user trips index page with page navigation (first, last, previous, next, go-to-page) and configurable entries per page (10/25/50, default 10) (#195)
- New
/api/Trips/searchendpoint for paginated trip queries with text search and visibility filtering
- Trip index page now loads data via AJAX instead of server-rendering all trips, improving performance for users with many trips
- Trip index page now shows a stats summary bar (total, public, private counts) that updates dynamically with search and filters (#194)
- Fix trip images appearing broken until first cache warm-up runs (#193)
- Reduce debounce delay from 5 minutes to 1 minute
- Add immediate mode (~5 seconds) for first-time image introductions
- Schedule warm-up on trip creation, cloning, and API trip updates (previously missing)
- Add loading="lazy" to proxied images in trip notes to defer off-screen image loading
- Fix Wikipedia/Wikimedia images returning 403 when proxied (missing User-Agent header)
- Use admin-configurable image cache expiry for browser Cache-Control headers instead of hardcoded 24h
- Add cache-busting to trip cover image URLs to prevent stale browser cache after URL changes
- Max proxy image download size is now admin-configurable (default raised from 20 MB to 50 MB)
- Improved image cache read performance by removing global lock serialization from cache hits
- LastAccessed updates are now conditional (only when stale >1 hour) reducing DB writes
- Added background cache warm-up: external images in notes and cover images are pre-cached 5 minutes after trip/region/place/area save (debounced)
- Extracted ImageProxyService for shared image fetch+optimize+cache logic
- Extracted ImageProxyHelper utility (IsUrlAllowed, ComputeImageCacheKey, OptimizeImage) to fix inverted service→controller dependency
- Added dedicated tests for HtmlHelpers.ExtractExternalImageUrls and CacheWarmupScheduler TOCTOU fallback path
- Fixed map snapshot URL returning 404 due to query string not being stripped from file path
- Added server-side proxy rewriting for external images in trip/region/place notes HTML
- Notes images now load through /Public/ProxyImage cache endpoint instead of directly from external servers
- Routed public cover images through ProxiedImageCacheService disk cache instead of raw 302 redirects
- Cover images in public trip grid, list, and Viewer hero are now served via /Public/Trips/{id}/CoverImage endpoint
- Cached cover images benefit from SSRF protection, ImageSharp optimization, ETag/304 support, and LRU eviction
- Private (non-public) trip cover images in Viewer continue to load directly from external URL for the owner
- Extracted shared FetchAndCacheImage pipeline used by both ProxyImage and CoverImage endpoints
- Fixed copy cover image and map snapshot URL options showing on public trip page to non-owners (#181)
- These options now only appear for the trip owner in the Viewer dropdown
- Added visual hint in backfill "Consider Also" tab for suggested locations already linked to the trip (#183)
- Suggested locations whose place is already confirmed or existing show a green lightbulb icon with tooltip
- Added search and filter functionality to user trips index page (#182)
- Search field filters trips by name and notes with debounced input
- Tri-state radio filter for All/Public/Private trip visibility
- Client-side filtering with combined AND logic
- Added public endpoints for trip cover image and map snapshot (#181)
- Added GET /Public/Trips/{id}/CoverImage — 302 redirect to cover image URL
- Added GET /Public/Trips/{id}/MapSnapshot — serves map snapshot JPEG directly
- Added GET /api/trips/public/{id}/images — JSON metadata with absolute image URLs
- Extracted shared rate limiting utility (RateLimitHelper) from TripViewerController and TilesController
- All new endpoints are rate limited for anonymous users
- Added copy URL options for cover image and map snapshot in trip Viewer dropdown and User Trip Index public dropdown
- Added area stats to trip summaries in list, grid, and quick preview views (#179)
- Added site-wide back-to-top button that appears after scrolling, with smooth scroll and theme support (#175)
- Fixed trips with no cover image showing broken image instead of map snapshot fallback in grid view (#176)
- Added map snapshot fallback to list view cover image column for trips with coordinates but no cover image (#176)
- Added disk-cached image proxy with LRU eviction for proxied images (#169)
- Added SSRF protection to ProxyImage endpoint blocking private IPs and non-HTTP schemes (#169)
- Added Cache-Control and ETag headers to proxied images and tile responses (#169)
- Added response compression middleware (Brotli + Gzip) (#169)
- Added admin-configurable image cache size limit and expiry duration (#169)
- Added image cache stats to admin settings dashboard (#169)
- Updated deployment docs and scripts for ImageCache directory (#169)
- Added trip progress share link toggle and copy button to trip Viewer page (#170)
- Added copy progress link option to trip Index public dropdown (#170)
- Fixed flaky SSE broadcast test timing on slow CI runners
- Fixed public trips grid view title unreadable in dark theme (#168)
- Fixed search clear button not cancelling pending debounce timer (#166)
- Fixed region headers not respecting dark theme in trip analysis modal
- Added inline clear button to analysis search field
- Improved trip analysis: group results by region and place name across all tabs (#163)
- Added fuzzy search filtering across all analysis tabs (#163)
- Fixed duplicate suggestions in Consider Also tab (#163)
- Increased analysis modal list height responsively for better data visibility (#163)
- Fixed EF Core warnings for First/FirstOrDefault without OrderBy on ApplicationSettings queries (#159)
- Fixed latent crash in LocationImportController when ApplicationSettings table is empty
- Added deterministic ordering to in-memory GroupBy deduplication patterns
- Fixed frontend.config.yaml missing from publish output causing startup warning (#160)
- Upgraded MvcFrontendKit from 1.0.0-preview.24 to 1.0.0
- Bumped HtmlSanitizer dependency from 8.1.870 to 9.0.892 (PR #158)
- Fixed API logging privacy for production release (#157)
- Changed authentication success logs to Debug level (silent in production)
- Removed usernames from logs, replaced with UserId
- Removed token info from success logs (retained in failure logs)
- Downgraded routine operation logs to Debug level
- Restructured documentation for open-source release (#146)
- Added 50+ screenshots throughout user and developer documentation
- Added Docsify theme with Wayfarer brand colors (teal/coral)
- Added local docs serving at /docs/ via ASP.NET static files
- Added Docs and Mobile links to navigation and footer
- Fixed broken internal documentation links
- Fixed missing API endpoint documentation (Icons, Tags, Users, Visit, Backfill)
- Simplified technical jargon in user-facing documentation
- Updated home page tagline to "Track Your Timeline - Manage Your Trips"
- Improved 404 page with larger transparent logo and bigger text
- Added centralized Wikipedia search utility with dual search strategy (#142)
- Combines geosearch and text search for better Wikipedia article discovery
- Migrated 8 files to use new shared module, removing ~600 lines of duplicate code
- Added place context map modal to trip visit analysis (#139)
- Map shows place marker and location pings that contributed to the match
- Includes ruler measurement tool, auto-fit bounds, and ping tooltips with details
- Added "Consider Also" suggestions feature to backfill analysis (#134)
- Added 4-tab interface for backfill modal: Confirmed, Consider Also, Stale, Existing
- Added cross-tier evidence logic to catch near-miss visits while filtering GPS noise
- Added SuggestedVisitDto with tier hit counts and suggestion reasons
- Added VisitedSuggestionMaxRadiusMultiplier setting (default 50×, configurable 2-100×)
- Added derived suggestion tier properties (Tier 1-3 radii and hit requirements)
- Added Source property to PlaceVisitEvent to track visit origin (realtime, backfill, backfill-user-confirmed)
- Added user check-in detection as strong signal for suggestions
- Added admin settings UI for suggestion multiplier with derived tier info panel
- Added unique index on PlaceVisitEvents (UserId, PlaceId, Date) to prevent duplicates at DB level
- Added chunking for batched spatial queries when places > 10,000 (PostgreSQL parameter limit)
- Added CancellationToken propagation to individual place analysis queries
- Added frontend validation for date range (fromDate must be ≤ toDate)
- Fixed potential KeyNotFoundException with TryGetValue pattern for region lookups
- Added Visit Backfill feature to analyze location history and create visit records (#104)
- Added backfill preview with new visits, stale visits, and existing visits sections
- Added confidence scoring based on location count and proximity
- Added stale visit detection (place deleted/moved beyond radius)
- Added manual visit deletion with checkboxes in existing visits
- Added select/deselect all functionality for visit selections
- Added action summary showing what will happen on Apply
- Added Clear All Visits option in trip dropdown menu
- Added navigation from Visit to underlying Location records (#127)
- Added Relevant Locations card on Visit/Edit page
- Added Locations column with lazy-loaded counts in Visit Index
- Added visit notification cooldown setting to reduce SSE spam (#128)
- Fixed duplicate visit prevention with timezone-aware date comparison
- Fixed duplicate detection to check by PlaceNameSnapshot in addition to PlaceId
- Fixed settings persistence for cooldown and rate limit settings (#128)
- Added location metadata fields: accuracy, speed, altitude, heading, source (#121)
- Added import deduplication to prevent duplicate location entries (#121)
- Added metadata support to all location exporters (GeoJSON, CSV, GPX, KML)
- Added metadata parsing to GPX, KML, GeoJSON, and CSV importers
- Added capture metadata display to Location Edit view
- Added test coverage for metadata parsing and deduplication boundaries (#124)
- Fixed Source field extraction in GeoJSON and CSV parsers
- Removed location timestamp unique index that caused import failures (#125)
- Added inline activity view/edit mode for location modals and tables
- Added table activity editing with preselected activity values
- Added cookie-auth fallback for location activity updates
- Added admin tile provider settings with presets, custom templates, and API key support
- Added tile provider validation and cache purge on provider change
- Added dynamic map attribution from the active tile provider
- Added tile request rate limiting for anonymous users (configurable, default 500/min per IP)
- Added X-Forwarded-For support for correct IP detection behind reverse proxies
- Added tile coordinate validation (z: 0-22, x/y: 0 to 2^z-1)
- Fixed XSS vulnerability in tile provider attribution via HTML sanitization (#115)
- Fixed race condition in tile cache size tracking with Interlocked operations (#115)
- Fixed API key exposure in tile service logs via URL redaction (#115)
- Fixed X-Forwarded-For spoofing by only trusting header from localhost/private IPs (#115)
- Fixed race condition in rate limiter with atomic ConcurrentDictionary operations (#115)
- Fixed rate limiter TOCTOU on window reset with CompareExchange (#115)
- Fixed tile cache lock not being shared across scoped service instances (#115)
- Fixed tile cache size not initialized from database on startup (#115)
- Fixed file read race condition after CacheTileAsync (#115)
- Fixed synchronous DB query in GetLruCachedInMbFilesAsync (#115)
- Fixed group map selection filters to honor Show/Hide All and historical visibility (#117)
- Security: Added HtmlSanitizer for safe attribution rendering
- Security: Added CSRF protection to cache deletion endpoints (#115)
- Security: Added anti-forgery tokens to cache deletion AJAX calls (#115)
- Added CHANGELOG.md
- Fixed popup dark theme styling
- Fixed API endpoint DTO responses (#101, #102)
- Added location idempotency keys for duplicate prevention
- Fixed area notes layout stretch
- Added GPS accuracy threshold filter for location logging
- Added PUT endpoint for updating trip areas
- Expanded admin threshold options (time and distance)
- Fixed GPS accuracy threshold persistence (default now 50m)
- Fixed duplicate location markers from race conditions (#93)
- Moved threshold display to User Settings page (#85)
- Reduced check-in rate limit from 30s to 10s
- Added user display name in navigation
- Fixed groups marker popup showing wrong user
- Fixed dark theme QR code readability for 2FA
- Exposed accuracy and speed properties in location views
- Improved marker clustering (exclude live/latest markers)
- Fixed dark theme inconsistencies across multiple views
- Fixed live-to-latest marker transition
- Added API token hashing for secure storage
- Added account lockout to prevent brute-force attacks
- Fixed hashed token authentication for mobile
- Added secrets management via systemd environment variables
- Added real-time job status updates via SSE
- Added job control panel (pause/resume/cancel)
- Added mobile visits recent endpoint for background polling
- Added 3-minute threshold option in admin settings
- Fixed orphan visit cleanup
- Added Visit management feature
- Added visit started SSE notifications
- Fixed visit search case sensitivity
- Fixed visit image sizing and marker size
- Added trip Areas feature with notes and images
- Added route progress tracking
- Added trip Places with route segments
- Added drag-to-reorder for places
- Added trip creation and basic editing
- Added trip privacy controls (public/private)
- Added location clustering for performance
- Added cluster statistics modal
- Added dark theme support
- Added theme toggle in user settings
- Added location search with filters
- Added date range filtering
- Added Google Timeline JSON import
- Added location export (JSON)
- Added reverse geocoding for locations
- Added location editing
- Added hidden areas feature for privacy
- Added Groups feature for sharing locations
- Added group invitations
- Added live location tracking via SSE
- Added latest location marker
- Added public timeline sharing
- Added embeddable timeline widget
- Added user statistics dashboard
- Added API token management
- Added mobile app authentication
- Added location logging API endpoint
- Added distance and time thresholds
- Added user registration and login
- Added two-factor authentication
- Added basic map view with OpenStreetMap tiles
- Added tile caching for fair use
- Added location display on map
- Added location CRUD operations
- Initial project setup
- Basic ASP.NET Core MVC structure