- README сокращён с 449 до ~200 строк
- Добавлены бейджи (Python, Node, Docker, лицензия) и оглавление
- Раздел возможностей — акцент на LLM-перевод контента БД как главную фичу
- Enterprise Clean вынесен в docs/enterprise-clean.md
- Авторизация, мониторинг, обновление — сокращены до минимума
- Создан LICENSE (MIT)
- Создан CONTRIBUTING.md
- Примеры переведены в промышленный контекст
- 2df63b7ce038: was checking _column_exists but not _table_exists.
If llm_providers/validation_policies/llm_validation_results don't
exist (fresh DB), _column_exists returns False and add_column crashes.
Now checks _table_exists first.
- a1b2c3d4e5f6 (20260603_add_token_limits_to_llm_providers): no guard
at all. llm_providers is created by create_all() at runtime. Guard
matches pattern used by ed28d34edde7, 9f8e7d6c5b4a and others.
dataset_review_sessions (and potentially other tables in FK_DEFS) are
created at runtime by init_db() → Base.metadata.create_all(), not by
Alembic migrations. On fresh databases, these tables don't exist when
migrations run, causing ALTER TABLE to crash.
Fix: check table existence before operating on each FK. If a table
doesn't exist, create_all() will create it with the correct FK
definition (the model already has ondelete='CASCADE').
This is the same pattern used by other migrations (9f8e7d6c5b4a,
ed28d34edde7, c9d8e7f6a5b4, 86c7b1d6a710) for create_all()-only tables
like llm_providers, roles, validation_policies, llm_validation_results.
- Add e1f2a3b4c5d6 migration: create dataset_review_sessions with FK
ondelete='CASCADE' (matching the model). Previously created at runtime
by init_db() → create_all().
- Update f0e9d8c7b6a5: change down_revision from a5b6c7d8e9f0 to
e1f2a3b4c5d6 so the table exists when this migration runs.
- All other create_all()-only tables (llm_providers, roles,
validation_policies, llm_validation_results) already have guards
(_table_exists()) in their respective migrations.
- Add d1e2f3a4b5c6 migration: create task_records table matching the
TaskRecord model (previously created at runtime by init_db() via
Base.metadata.create_all).
- Update a5b6c7d8e9f0: down_revision now points to d1e2f3a4b5c6, so
task_records exists when this migration runs. Remove the
inspector.has_table() guard (no longer needed).
- create_all() in init_db() becomes a no-op for task_records — the
table is now fully managed by Alembic.
svelte-markdown@0.4.1 declares peer dependency svelte@^4.0.0, but the
project uses svelte@^5.43.8. In clean Docker builds, npm ci strictly
validates peer deps and fails. Adding --legacy-peer-deps works around
the incompatibility until svelte-markdown supports Svelte 5.
- Remove overflow-x-auto/scrollbar-thin from translate job tabs, use flex-1 to fill width
- Remove redundant PROD badge from TopNavbar
- Move PROD context indicator into breadcrumbs row (right-aligned compact badge)
- Remove full-width warning banner and red left border from page content
Dashboard links in DatasetPreview were constructed without env_id query
parameter, causing 'Отсутствует ID дашборда или окружения' error when
clicking through from dataset detail. Use ROUTES.dashboards.detail()
helper which correctly appends ?env_id= to the URL.
- build.sh bundle_release() now builds ss-tools-agent:{tag} from
docker/Dockerfile.agent alongside backend and frontend
- Generated docker-compose.enterprise-clean.yml includes agent service
(image: + pull_policy: never, port 7860, depends_on: backend)
- Manifest .txt and .json include agent image fields
- sha256sums and load instructions updated for three images
- Static docker-compose.enterprise-clean.yml adds agent service (build
from source) for local 'up enterprise-clean' profile
Add full debugging chronicle (8 steps) documenting the JWT auth root cause:
- False hypotheses eliminated: FAB permissions, CSRF, different servers
- Root cause: Superset ignores SQLALCHEMY_DATABASE_URI env vars entirely
(uses only superset_config.py via SUPERSET_CONFIG_PATH)
- Three-component fix: psycopg2-binary + superset_config.py + Docker bridge IP
- Comparison table: what works vs what doesn't (8 approaches tested)
- Version comparison: 4.1.2 vs 6.1.0 (6 characteristics)
Remove incorrect FAB-permission hypothesis from earlier version.
Root cause: Superset 4.1.2 ignores SQLALCHEMY_DATABASE_URI env var —
always falls back to SQLite. With separate init+web containers, the
init container's SQLite DB is lost → web container has no admin user
→ JWT login returns 401.
Fix:
- Install psycopg2-binary at container start (python -m pip install)
- Create /tmp/superset_config.py via heredoc that reads SUPERSET_DB_URI
- Set SUPERSET_CONFIG_PATH=/tmp/superset_config.py
- Use Docker bridge IP (not localhost) for Postgres from inside container
Now all 10 integration tests pass, including:
- test_jwt_login_success (access_token + refresh_token)
- test_jwt_authenticated_api_call (Bearer token → /api/v1/dashboard/)
- 4 health checks + 2 form-auth + 2 client construct
The Gradio agent (run.py) crashed with OSError when port 7860 was
already occupied by a previous instance. Added _find_free_port() that
scans up to 100 ports from the configured GRADIO_SERVER_PORT and picks
the first available one, logging a warning on fallback.
Contract updates:
- AgentChat.Run: [C:3] [TYPE Module] (was C2/Function), added
@RATIONALE, @REJECTED, @SIDE_EFFECT for port-finding logic
- AgentChat.GradioApp: added @RATIONALE, @REJECTED
- AgentChat.LangGraph.Setup: added @REJECTED, deduplicated @RELATION
- AgentChat.Tools: added @RATIONALE
httpx.Response does not have .ok attribute (that's requests.Response).
Async migration missed this: _llm_async_http.py used response.ok in two
places, causing 502 errors when LLM API responded.
Fix: response.ok -> response.is_success
Root cause: AsyncAPIClient.request() passes its parameter directly
to httpx.AsyncClient.request(json=data). When callers pass a pre-serialized
JSON string (data=json.dumps(dict)), httpx re-encodes it via json.dumps(),
resulting in a double-encoded JSON string body instead of a JSON object.
This caused ALL POST/PUT requests with string data to fail — Superset received
a JSON string instead of a JSON object, returning GENERIC_BACKEND_ERROR
('dictionary update sequence element #0 has length 1; 2 is required').
Fix: if data is a string, pass it via httpx parameter (raw body);
if it's a dict/list, pass via for automatic encoding.
Affected callers (6 files) now correctly send JSON objects:
- preview_executor.py: chart data requests
- superset_executor.py
- _run_source.py
- _datasets.py: update_dataset
- _datasets_preview.py: compile_dataset_preview
- _dashboards_write.py
Also simplified preview_executor.fetch_sample_rows back to single-strategy
(chart data API only) since the root cause is now fixed.
1. preview_translation route: missing await on async preview_rows()
- preview_rows() is async def, called without await
- returned coroutine object instead of result -> 'coroutine not iterable' error
2. MultiSelect.svelte: opt.label -> opt.name
- option type is {code, name} but template used {opt.label}
- rendered empty spans instead of language names
debug.py: _test_db_api and _get_dataset_structure were already async def
but called SupersetClient methods (authenticate, get_databases, get_dataset)
without await. Added await to 4 calls.
task_logger.py: _add_log callback is async def but _log() called it without
await, silently dropping all task log messages (RuntimeWarning: coroutine
never awaited). Changed to fire-and-forget via asyncio.ensure_future when
a running event loop is available, drops gracefully otherwise.
executor.execute_run() is async def but was called without await in
TranslationExecutionEngine.execute_run(). This returned a coroutine
instead of a TranslationRun, causing:
'coroutine' object has no attribute 'status'
This made every translation run fail in the background execute path.
run_translation was def (sync FastAPI handler runs in thread pool with no event
loop), but its body calls asyncio.create_task(_background_execute()) which
requires a running event loop. Changed to async def so FastAPI runs it on the
event loop directly.
Error: 'Run failed: no running event loop'
get_dashboards_page_async() no longer exists — the sync/async split was
removed and the method is now simply get_dashboards_page() (already async).
Calls in dashboard slug resolution and git helpers were using the old name.
This caused AttributeError at runtime, making all slug-based dashboard
lookups fail with 'Dashboard not found'.
SupersetClientBase was missing aclose() method. AsyncSupersetClient had it
via override, but code creating SupersetClient directly (e.g. dashboard tasks
history route) would fail with AttributeError on await client.aclose().
All test methods calling validate_target_table_schema now async def + await.
Mocked async methods (resolve_database_id, execute_and_poll) use AsyncMock
instead of MagicMock since await on MagicMock raises TypeError.