Documents the seed YAML format (fields, category enum, expected
short/medium/long narration length) with one real filled-in example
and a fill-in-the-blank placeholder block, so new locations can be
submitted in a format seed_loader.py already understands and loads by
upserting on slug.
The "found N places" overlay text (and the checking/no-places states)
used the theme's default light text color, which washed out against
the now-real, mostly light-colored OSM map tiles. Fixed with a fixed
dark color (Mocha.Crust) for text overlaid on the map.
Added a toggle button (top bar, visible once places are found) that
switches the start screen between the map and a plain list preview of
the found places (reusing PlaceCard in its neutral/UPCOMING state) —
lets the user see what's "ready to read" before committing to Start.
Verified: testDebugUnitTest passes, assembleDebug produces a working
APK, sent to Telegram.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Getting a working mobile-SDK key from 2GIS turned out to require a
sales-mediated B2B process (Platform Manager subscription, demo keys
explicitly excluded from mobile SDK use per their own docs), not a
quick self-serve signup. Swapped to osmdroid instead: free, no API
key or account, works immediately.
CityMapView (replacing DgisMapView/DgisSdkProvider) wraps osmdroid's
View-based MapView via AndroidView, forwarding lifecycle events, with
markers for the user's location and each nearby place. Configured
required OSM tile-usage-policy user agent + app-private tile cache in
GuideCityApp. Removed the now-unused DGIS_API_KEY plumbing and the
arm64-only ABI filter (osmdroid has no heavy native libs, so it's not
needed) — APK is back down to ~19.6MB from ~69MB.
Verified: testDebugUnitTest passes, assembleDebug produces a working
APK, sent via the Telegram bot (previously blocked by its 50MB limit
with the 2GIS build).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Added the actual ru.dgis.sdk:sdk-map + compose-map dependencies (from
https://artifactory.2gis.dev/sdk-maven-release) instead of the Canvas
placeholder guess. Verified the exact API surface by downloading the
AARs directly and inspecting classes with javap (DGis.initialize,
MapOptions, MapComposableState/MapComposable, CameraPosition/GeoPoint)
rather than relying on possibly-stale docs.
DgisSdkProvider lazily calls DGis.initialize() once per process,
catching failure so a missing key doesn't crash the app; DgisMapView
renders the real MapComposable when that succeeds, falling back to
the old placeholder canvas otherwise.
Important finding: the existing DGIS_API_KEY (2GIS's public REST/JS
API key format) does NOT work with this native SDK. It requires a
separate dgissdk.key file issued per-app-package from dev.2gis.com,
placed in app/src/main/assets/ (gitignored). Documented in README.
Without it the app still runs fine on the placeholder map.
Also restricted ndk.abiFilters to arm64-v8a — the SDK's native libs
otherwise balloon the APK from ~19MB to ~190MB.
Verified: testDebugUnitTest passes, assembleDebug produces a working
(arm64-only, ~69MB) APK. Not runtime-verified against a real map key
or device — no emulator/device available in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three UX fixes:
1. The placeholder map was always rendered with userLocation=null and
places=emptyList() — MapScreen never passed the actually-resolved
location/nearby list to DgisMapView, so it only ever showed the
"waiting for location" placeholder text. MapViewModel now exposes
resolvedLocation and nearbyPlaces, wired through to the map. (Note:
this is still the Canvas-based placeholder, not the real 2GIS SDK —
that integration is still pending, as flagged since the skeleton
was first built.)
2. GuideScreen's Tinder-style pager only showed one card at a time,
which read as "it only found one place" even with 5 nearby. Swapped
it for a plain scrollable list (auto-scrolls to the active/narrating
item); tapping an item opens PlaceDetailScreen, which now owns the
swipe-between-places interaction instead. PlaceDetailViewModel
fetches the same nearby-ordered list GuideViewModel uses (falling
back to a single non-swipeable place when opened from
Favorites/search for something outside that list).
3. Narration now leads with the place's name and distance from the
user before the body text (util/Narration.kt), in both the guide
list and the detail swipe view.
Verified: testDebugUnitTest passes, assembleDebug produces a working
APK, sent to Telegram.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
After resolving location, the map screen now checks how many places
are nearby (same server-side iterative radius expansion the guide
screen uses) and shows "Found N places within Rm" with a Start button,
instead of jumping straight into narration. The search cap is raised
to 10km (was 5km) — if nothing turns up within that, a "nothing found"
message is shown instead.
This also addresses a UX report: the guide screen's Tinder-style card
stack only shows one card at a time by design, which read as "it only
found one place" even when 5 were actually there. Added a "1 / 5"
position indicator to the card stack so it's clear there's more to
swipe through, and the new summary screen surfaces the real count
upfront regardless.
Verified: ./gradlew testDebugUnitTest passes, :app:assembleDebug
produces a working APK, sent to Telegram.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause of the 502: the host's disk filled up (down to ~511MB
free), which appears to have restarted the Docker daemon. Every other
service on this shared host came back up automatically (they all have
restart policies); guideCity's three containers didn't and stayed
down. Freed ~3.2GB via `docker builder prune` + `docker image prune`
(safe: build cache and dangling images only, no running containers
touched) and brought the stack back up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Metro Solntsevo, the district's central park/pond (Bogdanova St),
Meshchersky forest park, the Levenson dacha (a Shekhtel-designed 1900
Art Nouveau landmark), and the Peredelkino writers' village — all
researched with real coordinates/history, in RU/EN x short/medium/long.
Verified against the live nearby endpoint: all 5 are found from the
user's actual coordinates (55.662303, 37.425486), radius expanding to
5100m to cover the farthest (Peredelkino village, ~5012m out).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
scripts/notify_telegram.sh sends a file (e.g. the debug APK) to a
Telegram chat via Bot API sendDocument. Reads TELEGRAM_BOT_TOKEN and
TELEGRAM_CHAT_ID from .telegram.env (gitignored, real credentials
never committed) or the environment. Verified end-to-end: sent the
current debug APK and confirmed delivery.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Verified reachable end-to-end (health/nearby endpoints respond
correctly through the reverse proxy) and set as the preferred
API_BASE_URL for testing from a physical device on any network,
ahead of the LAN-IP and emulator-loopback fallbacks.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Log.d/i/w/e calls covering location resolution, TTS lifecycle and
errors, nearby/search network calls and their failure paths, and
screen-level state transitions. OkHttp's logging interceptor now
routes through Log (tag "OkHttp") instead of println for consistent
filtering. Enabled testOptions.unitTests.isReturnDefaultValues so
android.util.Log calls don't crash plain JVM unit tests.
Added mockk + kotlinx-coroutines-test and a GuideViewModelTest suite
covering the load-success, load-failure, and retry paths — including
a regression test for the infinite-spinner bug (isLoading must clear
and errorMessage must be set on a failed nearby-search call, not left
hanging). Verified: ./gradlew testDebugUnitTest passes (4/4).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Request-level logging middleware (method/path/status/duration),
per-module loggers in routes/services (nearby search radius
expansion, 404s, name search), LOG_LEVEL setting wired through
docker-compose, and seed_loader converted from print() to logging.
Verified: pytest passes, and manually confirmed log lines appear for
both successful and 404 requests via `docker compose logs api`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two bugs combined to cause this: (1) Android blocks plain-HTTP traffic
by default since API 28, so every call to our HTTP-only local backend
was throwing and being silently swallowed; (2) GuideViewModel's
loadNearby() returned early on that exception without ever setting
isLoading = false, so the spinner never cleared and no error showed.
Fix: debug-only manifest override (src/debug/AndroidManifest.xml)
enables cleartext traffic for local dev against the Docker backend;
GuideViewModel now always resolves isLoading and surfaces a
errorMessage + retry() the UI can act on instead of hanging forever.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The api service already binds 0.0.0.0:8000 via docker-compose, so it's
reachable from other devices on the same network without extra config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full app scaffold wired to the backend: onboarding, content-length
picker, map screen (permission flow + text-search fallback), guide
screen with a swipeable place-card stack driven by on-device TTS
narration and a 60s re-scan loop, place detail screen with
mute/favorite controls, favorites (Room-backed), and settings.
Hilt for DI, Retrofit+kotlinx.serialization for the API client,
DataStore for onboarding/content-length prefs, FusedLocationProviderClient
for location. The 2GIS MapKit integration is isolated behind
map/DgisMapView.kt (currently a placeholder) since its exact Maven
coordinates need confirming from the 2GIS developer portal.
Verified by actually building it: installed a minimal Android SDK
(platform 34 + build-tools, no emulator) and JDK 17 locally, then ran
:app:compileDebugKotlin, :app:assembleDebug (produced a real debug
APK), and :app:lintDebug (0 errors, 44 non-blocking warnings, mostly
"newer dependency version available"). Caught and fixed two real bugs
this way: a wrong Maven artifact for the Retrofit kotlinx.serialization
converter, and missing ExperimentalMaterial3Api opt-in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gorky Park (6), Red Square area (10), and Moscow-City (6) landmarks
with real historical/architectural facts, each with short/medium/long
narration text in Russian and English. Widened built_year to
varchar(128) since several entries need more than a bare year (e.g.
demolition/reconstruction ranges).
Verified end-to-end against the live backend: all 22 places load via
seed_loader, nearby search returns correctly distance-sorted results
with radius expansion in sparser areas (confirmed at Gorky Park,
900m), and place detail/search-fallback endpoints return full
bilingual content. Full pytest suite passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FastAPI service backed by PostgreSQL+PostGIS: cities/places/place_content
schema (normalized per-language, per-length content for easy future
re-ingestion), Alembic migration, an iterative-radius-expansion nearby
search endpoint, a name-search fallback for denied-location flows, and
a YAML seed loader CLI. Verified end-to-end against a live Docker
Postgres+PostGIS instance (migration, city seed, place/content CRUD,
and nearby radius expansion all confirmed working via curl).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Set up git repo with .gitignore and README describing the planned
backend (FastAPI + PostGIS) and Android (Kotlin/Compose) architecture.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>