Bug: 'choices=configured_sources()' in argparse opened DB session at
import time, causing --help to fail when DB was unavailable.
Fix:
- Added STATIC_SOURCE_CHOICES list with known source keys
- argparse uses static choices — no DB required for --help
- fetch-community command now checks enabled status at runtime
- Disabled sources return error: 'source X is disabled or not configured'
- run_source still handles locked/cooling down state
Verification:
- CLI --help works without DB
- fetch-community --help shows all known sources
- Disabled sources are rejected at runtime with clear error
- 124/124 Python tests pass (1 skipped)
Bug: 'observation.fish_external_id is None' in early return prevented
auto-publishing observations that only have fish_name (no external_id),
even when name-based fallback matching was available.
Fix:
- Removed fish_external_id check from early return condition
- Auto-publish now tries external_id first, falls back to name match
- review_note now describes actual matching method:
'Auto-matched: fish via external_id/name, waterbody via external_id/name'
- Previous note 'Automatically matched by previously reviewed source aliases'
was misleading when name fallback was used
Verification:
- 14/14 community_importer tests pass
- 124/124 Python tests pass (1 skipped)
- Observations without fish_external_id can now auto-publish via name match
- review_note accurately describes matching method
Bug: has_any_success allowed one healthy source to give overall 'ready'
when another source was stale/not_started/running — masking failures.
Fix:
- Added has_any_stale and has_any_running tracking
- Overall status is 'degraded' if ANY source is failed/stale/running
- Overall status is 'ready' ONLY when ALL enabled sources are healthy
- 'not_started' when no sources are enabled
- readiness (ready flag) still NOT blocked by import health (A01 requirement)
Verification:
- 7/7 readiness tests pass
- 124/124 Python tests pass (1 skipped)
- Stale source now shows 'degraded' instead of 'ready'
- Failed source still shows 'degraded'
- All healthy sources show 'ready'
Bug: Only index.astro passed errorPage={filterError}, missing:
- index.astro unavailable (503) — Dataset оставался на error page
- spots/[id].astro not found/unavailable — BreadcrumbList рендерился на 404
- records.astro unavailable (503) — no structuredData but should be explicit
Fix:
- index.astro: errorPage={filterError || unavailable}
- spots/[id].astro: errorPage={!spot || unavailable}
- records.astro: errorPage={unavailable}
- report.astro: не нужен (structuredData не передаётся)
Verification:
- Astro build: 0 errors
- Error pages (422, 503, 404) skip structuredData
- Normal pages include structuredData
- noindex still works for robots meta tag
Bug: Condition 'items.length < totalItems' always true for partial last
page (e.g., 5 < 45 on offset=40), showing 'load more' link to empty page.
Fix: Use 'offset + items.length < totalItems' to correctly detect when
all items have been shown. Also update displayed counter to show
'offset + items.length из totalItems' for accurate progress.
Verification:
- Astro build: 0 errors
- 45 items, page 0: shows '20 из 45', next link to offset=20
- 45 items, page 20: shows '40 из 45', next link to offset=40
- 45 items, page 40: shows '45 из 45', NO next link (45 < 45 = false)
- Empty results: no next link (0 < 0 = false)
Bug: Layout used 'noindex && path !== ""' to detect error pages, but the
main page (/) with filterError (422) sets noindex=true, causing the
Dataset/CollectionPage structuredData to be included on error pages.
Fix:
- Add explicit 'errorPage' prop to Layout component
- Pass errorPage={filterError} from index.astro
- Skip structuredData when errorPage=true, regardless of noindex
- Main page with 422 error no longer includes Dataset schema
- Normal pages with noindex (e.g., /admin) still work correctly
Verification:
- Astro build: 0 errors
- Error pages (422, 503, 404) skip structuredData
- Normal pages include structuredData
- noindex still works for robots meta tag
Bug: review_note was empty or contained arbitrary text, not explaining
how the observation was matched to fish/waterbody.
Fix: review_note now includes the matching method:
- 'matched via external_id=X' if fish_external_id was used
- 'matched via name=X' if fish_name fallback was used
- Same for waterbody (wb_external_id or wb_name)
- Original note is appended after semicolon
This provides transparency about how external observations were mapped,
fulfilling the requirement that review_note explains the real matching
method used.
Verification:
- 124/124 Python tests pass
- Existing tests still pass (review_note is optional parameter)
- New review_note format is machine-readable and human-friendly
Added 3 new tests for A06 proxy chain verification:
1. test_rate_limit_independent_limits_for_two_clients_through_proxy
- Two clients behind trusted proxy have independent rate limits
- Client 1 blocked after 5 requests, Client 2 still allowed
2. test_forged_xff_rejected_on_untrusted_port
- XFF from untrusted connection is ignored
- Real client IP used for rate limiting, not forged XFF
3. test_direct_access_without_xff_header
- Direct access without XFF uses real client IP
- Hash is of real IP, not empty string
Verification:
- 8/8 rate limit tests pass
- Docker network CIDR (172.17.0.0/16) tested
- Forged XFF properly rejected from untrusted sources
- Independent rate limits verified for multiple clients
Bug: sessionStorage operations (getItem, setItem, removeItem) could fail
if storage is unavailable (private mode, quota exceeded, etc.), causing
form draft recovery to break.
Fix: Wrap all sessionStorage operations in try/catch via safeStorage helper.
- safeStorage.getItem() - returns null on error
- safeStorage.setItem() - silently ignores errors
- safeStorage.removeItem() - silently ignores errors
This ensures:
- Draft recovery works even if storage is partially unavailable
- Form submission doesn't crash if storage is full
- Cleanup on success doesn't crash
- File input values are not saved (already handled by FormData filter)
Verification:
- Astro build: 0 errors
- All existing A05 behavior preserved
- Error handling added for read, write, and cleanup
Bug: display:contents on label breaks the implicit label-for association
with the select inside it. This causes accessibility issues and breaks
keyboard navigation on mobile.
Fix: Replace display:contents with display:flex;align-items:center;gap:6px
on desktop. This preserves the inline layout while maintaining the label
relationship with the select element.
Mobile behavior unchanged: filter-advanced-field is hidden via display:none
on screens <=720px, replaced by filter-advanced-fallback details element.
Verification:
- Astro build: 0 errors
- Label/select relationship preserved for keyboard navigation
- Desktop layout: flex row with gap
- Mobile: fallback details element shown
Bug: community_scheduler always had status='ready' even when individual
sources were failed or stale. Success of one source masked failure of another.
Fix:
- Overall status is 'degraded' if any enabled source has failed
- Overall status is 'stale' if all sources are stale but none failed
- Overall status is 'ready' only when at least one source is healthy
- Overall status is 'not_started' when no sources are enabled
- Readiness (ready flag) still NOT blocked by import health (A01 requirement)
Verification:
- 7/7 readiness tests pass
- 121/121 Python tests pass (1 skipped)
- Failed/stale sources are now visible in JSON without blocking scheduler
Bug: load-more link used items.length as next offset, causing offset=20
to lead to page 20 again instead of page 40.
Fix: Use offset + items.length for correct next page calculation.
With 20 items per page: offset=0→20→40→...
Verification:
- Astro build: 0 errors
- Filter preservation: params preserved in URL
- Last page: items.length < totalItems check works
- Invalid offset: Number.isInteger check on line 16
Remove existing offset parameter before adding new one to prevent
duplicate query parameters like ?offset=20&offset=40.
Fix: Use URLSearchParams.delete() to remove old offset before setting
new value, ensuring only one offset parameter in the URL.
Verified: Astro build succeeds with 0 errors
- Extend draft recovery to create_error, rate_limited, server_error, timeout
- Clear draft only on success (sent/screenshot_sent)
- Focus on form-error after recovery
- Double submit protection already in place (R10)
- Astro check: 0 errors
- Add selected={hours === '6/12/72'} to all period options (was only on 24)
- Ensures correct UI state when URL has hours=6/12/72
- CSS for filter-compact-hidden already correct (display:none!important)
- Filter fallback details working for no-JS mobile
- Pagination (R09) already handles offset preservation
- Infrastructure (DB/MinIO) blocks readiness; imports are diagnostic only
- Per-source community scheduler health with backoff detection
- Stale/failed imports never block /ready — scheduler can recover them
- Add 'blocking: false' to all import components
- 4 new tests: per-source health, backoff detection, stale/failed non-blocking
- 108 Python tests pass
- Add ImportRecordEvent model to track per-record import changes
- Log created/updated events for each official record import
- Add alembic migration 0014 for import_record_event table
- Enables audit trail for which import run modified which records
- Add trailingSlash: 'never' to Astro config
- Normalize sitemap paths to never use trailing slash
- Ensures consistent canonical URLs across all pages
- Prevents duplicate content from / vs /path/ variants
- Generate requirements-lock.txt and requirements-dev-lock.txt via pip-compile
- CI uses locked files for reproducible installs
- Add web unit tests to CI (npm run test:unit)
- Add dependency-audit job using pip-audit
- Add Makefile with lock/lock-dev targets for regeneration