- RBAC: ensure_admin_role() guarantees the Admin role carries is_admin=True
(startup self-heal + create_admin promotion + role-is_admin UI checkbox in
admin/roles); update_role refuses to strip is_admin from the last admin role.
- WS: broadcast_maintenance_event is now awaited (3 sites) so maintenance
events actually reach clients (was an un-awaited coroutine RuntimeWarning).
- Pagination: MAX_PAGINATION_PAGES cap + clear error in fetch_paginated_data
to stop runaway loops on huge environments.
- Discovery: find_affected_dashboards and translate datasource picker filter
datasets/dashboards server-side (table_name/id filters, opr operator per
Superset OpenAPI) instead of full scans that hit the pagination token cap;
fallback to full scan when filters are rejected; virtual-dataset dedupe.
WebSocket endpoints now accept then close with real codes (4001 auth, 4003
permission) so clients detect auth failure via event.code instead of an opaque
403 handshake, ending the infinite reconnect storm. _authenticate_websocket
logs the actual JWT/API-key failure reason. Frontend WS consumers stop on
auth rejection and use capped exponential backoff for transient failures.
async_network.request() routes proxy 502/503/504 (HTML) responses to
NetworkError so migration/maintenance surface a clean 503 instead of a
500 JSON-parse traceback.
Systematic rename of all semantic anchors (#region, [DEF], @RELATION)
across 1400+ files — backend Python, frontend Svelte/TS, specs, docs:
- Flat anchors become Namespace.Module.Entity
- @RELATION references updated to match new anchor paths
- Zero business logic changes
Introduce a deployment recording system to track dashboard versions
across environments and improve the Git management user experience.
- Add `Deployment` model and Alembic migration to persist deployment
history.
- Implement `GitDeploymentRecorder` and `GitFingerprint` plugins to
automate deployment logging and content hashing.
- Add `get_deployment_status` API endpoint to retrieve real-time
environment states.
- Refactor `GitLifecycleHeader` to prioritize Call-to-Action (CTA)
buttons and improve visual hierarchy.
- Update `GitWorkspacePanel` to emphasize version saving and
streamline commit workflows.
- Enhance `GitEnvironmentTimeline` with deployment status integration,
collapsible UI, and improved theme consistency.
- Add auto-navigation logic in `GitManagerModel` to guide users to
relevant tabs based on recommended actions.
- Clean up obsolete documentation and update i18n strings for
git visualization features.
log_requests middleware (BaseHTTPMiddleware) showed no-trace
because anyio.create_task_group() in Starlette 0.50.0 does not
always propagate ContextVar from raw ASGI TraceContextMiddleware.
Fix 1: defensive get_trace_id() check at log_requests entry —
if empty, seed_trace_id() to ensure every request has one.
Fix 2: CotJsonFormatter used 'if error:' and 'if payload:' which
silently drop empty strings (str(e)='') and empty dicts.
Changed to 'is not None' checks — preserves all data.
Root cause: 12 EXPLORE-without-error entries from belief_scope's
exception handler where Exception() has empty message.
Create AsyncJobRunner — centralized bridge between APScheduler
(BackgroundScheduler, sync thread pool) and async coroutines.
Fixes:
- P0: execute_run() called without await from APScheduler thread,
causing coroutine to be silently discarded (root cause: no
translation history)
- P0: get_async_job_runner() deadlock when called from APScheduler
thread pool without running event loop
- P1: ID mismatch in disable_schedule/delete_schedule routes
(job_id passed instead of schedule_id)
- P1: asyncio.run() in APScheduler callbacks incompatible with
running event loop
- Delete unused llm_analysis/scheduler.py (not used in production)
Changes:
core/async_job_runner.py — new: AsyncJobRunner class
core/scheduler.py — use runner.run()/run_later()
translate/scheduler.py — use runner.run() for execute_run
mapping_service.py — remove unused BackgroundScheduler
dependencies.py — add get_async_job_runner() DI
app.py — init runner in lifespan
api/routes/migration.py — use runner.run()
_schedule_routes.py — fix ID mismatch
plugins/migration.py — use runner.run()
llm_analysis/scheduler.py — delete (unused)
tests: 151 new/updated tests, all passing
BaseHTTPMiddleware (Starlette 0.50.0) uses anyio.create_task_group() internally,
creating separate asyncio tasks for dispatch vs call_next. ContextVars set in
dispatch() were not visible to outer middleware like log_requests.
Converting to raw ASGI middleware ensures trace_id is seeded in the root task
context, visible to ALL middleware layers.
Key changes:
- Replace BaseHTTPMiddleware with raw ASGI __call__(self, scope, receive, send)
- UUID v4 validation: check parsed.version == 4 explicitly instead of relying
on uuid.UUID(hex=..., version=4) which silently mutates non-v4 UUIDs
- Add @RATIONALE and @REJECTED tags per semantics-core protocol
- Update app.py comment to document the architectural decision
When the backend restarts (deploy, crash, reload), the in-memory
TaskManager loses its queue. ValidationRun records left 'running'
in the DB would hang forever, blocking re-runs.
Fix: during lifespan startup, query all ValidationRun with
status='running' and force-stop them as FAIL with a clear summary.
This prevents the 'already has a running run' error after restarts.
1. Remove get_config_manager() call from translate_run_websocket
(@app.websocket /ws/translate/run/{run_id}) — was not imported
and unused. Also remove unused from sqlalchemy.orm import Session.
2. Fix prop name mismatch: showBulkReplace -> showPageBulkReplace
in RunTabContent. Use () for reactive two-way binding.
3. Add ('DRAFT') for status prop in RunTabContent.
Backend:
- Add /ws/translate/run/{run_id} WebSocket endpoint in app.py
- Streams structured run status every second (total_records,
successful_records, failed_records, progressPct, batch_count, etc.)
- Auto-detects terminal states (COMPLETED, FAILED, CANCELLED) and closes
- Authenticated via JWT/API key token query param
Frontend:
- Add getTranslateRunWsUrl() URL builder in api.js
- Update translationRunStore to use WebSocket instead of log stream
- WebSocket receives structured status JSON, updates store reactively
- Falls back to polling silently if WebSocket fails
- Keep polling as fallback for insert phase
After configure_logger() runs, logger.isEnabledFor(logging.INFO)
returns False despite level=10 (DEBUG). This is a CPython logging
framework anomaly that makes logger.info() silently drop messages.
Workaround: check isEnabledFor first; if False, write JSON directly
to sys.stderr bypassing the logging system entirely.
- Added logger.info() calls before each startup step (encryption key,
Alembic, init_db, admin bootstrap, scheduler, app complete)
- Previously only logger.reason() was used which logs at DEBUG level
and is invisible in production/development at INFO log level
- This fixes 'no backend logs visible' when running via ./run.sh
- Added missing from fastapi import status (global_exception_handler crashed)
- Moved TraceContextMiddleware to outermost (trace_id now available in log_requests)
- Enhanced global_exception_handler with query_params, client host
- Added logger.explore() in WebSocket auth except blocks (was silent pass)
- Fixed middleware registration order
- Added @app.exception_handler(Exception) that logs full traceback via
logger.exception() into superset_tools_app logger (visible in docker logs)
- Previously FastAPI/Starlette wrote unhandled exceptions to uvicorn.error
logger, making 500 errors invisible in docker logs
- Added JSONResponse import for the handler
- Simplified init_db() to only call Base.metadata.create_all() on all
three engines — no more _ensure_*() inline additive migrations
- run_alembic_migrations() now raises on failure (no safety net to
fall back to)
- All schema changes (add/drop/rename columns, data migrations) must
go through Alembic. create_all() handles new tables only.
@REJECTED _ensure_*() inline migrations removed because they:
- Duplicated Alembic logic without versioning
- Created hidden schema drift (columns existed in DB but had no
corresponding Alembic migration)
- Made audits and fresh DB provisioning unreliable
- Added alembic==1.18.4 to requirements.txt (was missing, transitive only)
- Added run_alembic_migrations() to startup_event in app.py — runs
before init_db() to apply all pending migrations
- Combined approach: Alembic for complex migrations first, then inline
_ensure_*() additive safety net in init_db()
- This ensures DB schema is always up to date on container startup
without manual intervention
HSTS (Strict-Transport-Security) is now opt-in via FORCE_HTTPS=true.
Without it enabled, no HSTS header is sent — safe for dev environments
and enterprise deployments behind HTTP-only proxies without HTTPS certs.
When FORCE_HTTPS=true, sends 'max-age=31536000; includeSubDomains'.
Enterprise recommendation: set HSTS at nginx/ingress level instead.
.env.example documents the risk: enabling without certs breaks site access.
TrustedHostMiddleware blocked Vite proxied requests (Host: localhost:5173
vs allowed_hosts list). Replaced with simple HSTSMiddleware that adds
Strict-Transport-Security header without blocking any requests.
L-2: HSTS middleware via TrustedHostMiddleware — restricts allowed hosts
to ALLOWED_ORIGINS list, prevents Host header injection.
L-4: AD group name validation — ADGroupMappingCreate.ad_group validated
with regex: DOMAIN\groupname or CN=...,DC=... format. Empty or
invalid characters rejected.
L-5: INITIAL_ADMIN_PASSWORD warning — log warning that env-var password
is visible via /proc to other processes on the same host.