Fixes UX bug: пользователь создаёт запись с validFrom на будущую дату
(e.g. 18.05), открывает catalog 15.05 — пустая таблица, никаких хинтов
о том что запись существует.
Backend (read-api):
- New endpoint GET /api/v1/{dict}/records/scheduled-summary
- Returns {count, nearestValidFrom} для записей с validFrom > now AND
validTo > now в текущем scope view
- Single-aggregate JPA query (COUNT + MIN), respect scope filter same way
as listActive — INTERNAL scheduled не учитывается для PUBLIC consumer'а
Frontend:
- New useScheduledSummary hook + ScheduledRecordsSummary type
- Empty-state в dictionaries.$name now shows: "Запланировано N, ближайшая
на DD.MM [Перейти к этой дате]" с CTA который set'ает time-travel на
nearestValidFrom
- i18n keys для ru/en plural forms
Связан с MR !219 (TimeTravelPicker 30-day future window) — теперь slider
позволяет дойти до даты записи, и empty-state hint позволяет узнать что
запись существует.
Дополнение к bbox: ?polygon=<GeoJSON> для произвольных полигонов
(ground stations coverage, custom AOI). Один из bbox/polygon —
взаимоисключающие; передача обоих → 400.
Repository:
- findActiveByPolygonGeoJson native query через PostGIS
ST_GeomFromGeoJSON(polygon) → ST_SetSRID(_, 4326). DB-side parsing,
не нужен jts-io-common.
GeoJsonPolygon validator (app-side, structural):
- Парсит JSON, проверяет {"type":"Polygon","coordinates":[[...]]}
- RFC 7946 closed-ring check: first point == last
- >= 4 points в каждом ring (3 вертекса + closing point)
- Поддержка holes (multiple rings)
- Coordinate range validation: lon ∈ [-180,180], lat ∈ [-90,90]
- DoS guards: MAX_JSON_BYTES = 1MB, MAX_POINTS = 50_000
- Canonical re-serialize (whitespace stripped) перед DB
Tests (13 в GeoJsonPolygonTest):
- Valid square + polygon with hole
- Reject empty/null/non-JSON/non-object/wrong-type/missing-coords
- Reject too-few points / unclosed ring
- Reject lon/lat out of range
- Reject malformed point [no lat]
- Reject too-large JSON
Total project tests: 161 → 174.
API now supports:
GET /api/v1/{dict}/records?bbox=west,south,east,north
GET /api/v1/{dict}/records?polygon={"type":"Polygon",...}
Альтум-задача (rect AOI) и геопортал (polygon AOI) разблокированы
от client-side full-fetch + filter.
v2 фича: Альтум-задача (заказ съёмки) использует bbox region. До
этого consumers fetch'или ВСЕ записи и filter'или client-side. Теперь
PostGIS ST_Intersects на server-side через GiST index.
API:
GET /api/v1/{dictionaryName}/records?bbox=west,south,east,north
Coords в SRID 4326 (longitude, latitude). Параметр optional —
отсутствует = старое поведение (full list).
BoundingBox helper (ordinis-read-api/spatial/):
- Парсинг "west,south,east,north" с валидацией
- Range checks: lon ∈ [-180,180], lat ∈ [-90,90]
- west <= east (anti-meridian crossing rejected в v1)
- south <= north
- Понятные error messages → 400 Bad Request через BadBboxException
Repository (ordinis-domain):
- findActiveByBbox native query, ST_MakeEnvelope(:west,:south,:east,
:north,4326) ST_Intersects на geometry колонке. GiST index hit.
- Записи с geometry IS NULL автоматически отфильтровываются.
Tests (12 в BoundingBoxTest):
- Valid parsing + negative coords + whitespace
- Reject empty/null/wrong-count/non-numeric
- Range validation (lon/lat bounds)
- Anti-meridian + inverted N/S detected
- Area calc sanity
Total project tests: 149 → 161.
Polygon GeoJSON support — отложен (нужен jts-io-common dep). Bbox
покрывает Альтум use case + 80% other consumers.
Spring Boot Micrometer по дефолту эмитит только _count/_sum/_max
для http.server.requests — без _bucket нельзя посчитать
histogram_quantile() в Prometheus, p50/p95/p99 latency дашборды
получают "No data".
Включён percentiles-histogram + кастомные SLO bucket boundaries
от 5ms до 5s — покрывают realistic range read-api JSON endpoints.
Server-side histograms точнее client-side percentiles (по которым
нельзя aggregate across instances).
Применить: build + push image + helm upgrade. После рестарта
ordinis-app/read-api/projection-writer Grafana дашборды покажут
latency графики.
Новый модуль ordinis-auth:
- OrdinisAuthProperties — public-key, issuer, requireAuthentication, allowQueryScope
- ScopeContext — резолвит current scopes из JWT claim 'scopes' (приоритет),
потом legacy ?as_scope=, потом anonymous=PUBLIC
- SecurityConfig — Spring Security OAuth2 Resource Server + RSA public-key
decoder. Если public-key пуст — JWT validation отключён (permissive
fallback); rollout без auth-сервера не ломает API.
Read controller теперь делегирует scope resolution в ScopeContext вместо
прямого парсинга ?as_scope=. Backward compat: legacy query параметр
работает пока ordinis.auth.allow-query-scope=true (по умолчанию true в
dev/staging, false в prod после интеграции с @nstart/auth).
Vault: public-key читается из secret/ordinis/cuod/jwt-public-key (key=key).
Spring Cloud Vault flatten'ит в property 'key', resolved в
ordinis.auth.public-key=${key:}.
Stack trace showed io.opentelemetry.exporter.internal.http.HttpExporter
sending to :4317 — Tempo on 4317 expects gRPC frames, gets HTTP, sends
RST. Switch endpoint to :4318 (Tempo OTLP HTTP receiver) and protocol to
http/protobuf so the SDK and Tempo speak the same protocol.
The gRPC config never took effect — Spring Boot SDK on micrometer-tracing-
otlp pulls in only the HTTP exporter by default; gRPC needs the explicit
opentelemetry-exporter-otlp jar.
K8s profile relied on shared/dev defaults for spring.datasource.url, but those
live inside the dev profile block — invisible when k8s profile is active.
Result: 'APPLICATION FAILED TO START — Failed to configure a DataSource'.
Move full datasource (and kafka/redis for projection-writer) wiring into the
k8s profile section. ENV vars come from Vault Agent injector
(ORDINIS_PG_USER/PASSWORD, ORDINIS_KAFKA_USER/PASSWORD, ORDINIS_REDIS_PASSWORD)
and chart-supplied ENV (ORDINIS_PG_HOST, ORDINIS_KAFKA_BOOTSTRAP, etc).
application.yml fallback URL was http://config-server.ordinis-dev:8888 — namespace
removed. Default now points to staging; SPRING_CLOUD_CONFIG_URI env still overrides
via Helm chart for any deployment.
ordinis-read-api модуль полностью реализован:
- OrdinisReadApiApplication main + EnableJpaRepositories/EntityScan
- LocaleNegotiator: RFC 7231 / Locale.LanguageRange.parse + Locale.lookup
по supported_locales справочника, fallback на default_locale,
поддержка Accept-Language: * (raw mode)
- LocaleAwareJsonFlattener: walk schema_json.properties, для x-localized
полей в data заменяет {locale:text} на string выбранной локали
с fallback на default_locale; recursive walk для nested objects
- RecordReadService: read с scope filtering + locale negotiation +
flatten, метрика ordinis_read_api_query_duration_seconds, scope
access enforcement (ScopeAccessDeniedException)
- DictionaryReadController: GET /api/v1/{name}/records/{businessKey}
и list, with @RequestHeader(Accept-Language), Content-Language
response header, Vary: Accept-Language; query param as_scope для
v1 (заменим на JWT scope claim позже)
- ReadApiExceptionHandler: 404 NoSuchElement, 403 ScopeAccessDenied,
400 IllegalArgument, 500 fallback
- application.yml profile-based: dev (port-forward, ddl-auto=none —
read-api не валидирует schema, это work writer/migrations), k8s
(Vault role ordinis-read-api, hikari read-only=true)
- logback-spring.xml: MDC включает 'locale' для Loki query
E2E (port 8081 параллельно с writer на 8080):
- GET MOSCOW-1 без Accept-Language → name=Москва-1, Content-Language: ru-RU
- GET MOSCOW-1 с Accept-Language: en-US → name=Moscow-1, Content-Language: en-US
- GET MOSCOW-1 с Accept-Language: fr,en-US;q=0.8,ru;q=0.5 → выбрал en-US (fr нет в supported)
- GET MOSCOW-1 с Accept-Language: * → raw {ru-RU,en-US}, _meta.locale=null
- GET list → массив с flatten для каждой записи
- GET NONEXISTENT → 404 not_found
- GET с as_scope=INTERNAL для PUBLIC записи → 403 scope_access_denied