98de628197
docs(adr): reference ADR-0009 SSL cert management findings
...
Production SSL failure for lite.ai.rusal.com confirmed: LLM_CA_CERT_URLS
not set → LLM CA certs not downloaded. System CA store has corporate
certs from /opt/certs but lite.ai.rusal.com uses a different CA.
Diagnostic script scripts/diag_container.py covers all 6 check categories
per ADR-0009: env, system CA, openssl capath/cafile, Python SSLContext,
httpx connectivity, encryption health.
Fix: set LLM_CA_CERT_URLS in .env.enterprise-clean or place the CA .crt
in ./certs/ on the host. Run diag_container.py to verify.
2026-07-06 20:27:28 +03:00
bcabe90e13
feat(scripts): add container diagnostic script for SSL cert + encryption health
...
Checks 6 categories:
1. Environment — ENCRYPTION_KEY, LLM_SSL_VERIFY, LLM_CA_CERT_URLS
2. System CA store — ca-certificates.crt, custom/LLM certs, hash symlinks
3. OpenSSL connectivity — cafile vs capath (per ADR-0009 Finding 7)
4. Python SSLContext(capath) — what _get_ssl_verify() uses
5. httpx(capath) — integration test
6. Encryption key health — decrypt test for all LLM provider api_keys
Usage:
docker cp scripts/diag_container.py superset-tools-backend-1:/tmp/
docker compose exec backend python3 /tmp/diag_container.py --target lite.ai.rusal.com:443
2026-07-06 20:27:03 +03:00
e7dbee563f
fix(ui): render KeyRecoveryWizard into body portal to avoid layout margin
...
Modal now mounts at document.body level via svelte mount/unmount.
Extracted WizardContent.svelte for portal rendering.
Eliminates 32px margin from page layout wrapper
(max-w-7xl/space-y-6/px-4 container).
Added KeyRecoveryWizard as portal wrapper, WizardContent as
the actual modal component.
2026-07-06 19:00:24 +03:00
6db49e45a6
xz -3
2026-07-06 18:26:31 +03:00
7f9e781873
fix(security): address QA/security audit findings — authorization, i18n, error safety
...
Critical (C1): Added has_permission('security', 'READ') to /health and
/fingerprint endpoints; has_permission('security', 'WRITE') to /recover.
Previously any authenticated user could enumerate encrypted secrets and
overwrite stored values.
High (Bug #1 ): Fixed recover endpoint — updated/failed status now correctly
reflects whether a non-empty replacement value was submitted. Empty values
now report 'skipped' instead of falsely 'updated'.
High (Bug #2 , #3 ): Replaced all hardcoded English strings in
KeyRecoveryWizard.svelte and SystemSettings.svelte with $t.settings.*
i18n lookups. Keys already existed in en/ru settings.json.
High (H1): Added _sanitize_error() helper to truncate + scrub exception
messages before logging, preventing potential secret leakage in log output.
Cleanup: Moved LLMProviderConfig import to module top. Instantiate
ConnectionService once before for-loop. Added @TEST_EDGE declarations
and @PRE/@SIDE_EFFECT to C4 contract.
Tests: 226 passed
2026-07-06 18:22:36 +03:00
2622431376
fix(i18n): remove duplicate closing braces in encryption recovery strings
2026-07-06 18:07:44 +03:00
5c59a2c79b
feat(security): add encryption health inventory and key recovery wizard
...
Backend:
- GET /api/security/encryption/health — inventory of all stored encrypted
secrets (LLM providers, DB connections, profile Git tokens) with
decrypt attempt and structured broken/healthy status
- GET /api/security/encryption/fingerprint — non-secret key fingerprint
- POST /api/security/encryption/recover — bulk replacement of
undecryptable secrets with partial_success semantics
Frontend:
- KeyRecoveryModel.svelte.ts — state machine (idle→scanning→
healthy/needs_recovery→editing→saving→complete/partial_success/error)
- KeyRecoveryWizard.svelte — tabbed dialog with LLM/DB/Git sections,
security guidance, re-encrypt command display, edit/save flow
- SystemSettings entry point — 'Check encrypted secrets' card with
fingerprint and broken count
- API methods: getEncryptionHealth, recoverEncryptedSecrets
- Types: EncryptionRecoveryTypes
- i18n: en/ru strings for recovery flow
Tests: 226 passed
2026-07-06 18:05:05 +03:00
556294aff5
fix(agent): pin gradio <6 to avoid breaking ChatInterface API changes
2026-07-06 16:18:55 +03:00
b6752e729e
fix(docker): add alembic to requirements-backend.txt, entrypoint needs it for migrations
2026-07-06 15:49:48 +03:00
48e3ff4503
refactor(env): unify Docker env vars — canonical AUTH_SECRET_KEY, remove JWT_SECRET fallback
...
Canonical variables:
- AUTH_SECRET_KEY — JWT signing key for backend + agent (was split across
AUTH_SECRET_KEY / JWT_SECRET)
- SERVICE_JWT — agent→backend service token
- No JWT_SECRET fallback: decoder fails with migration guidance if only
JWT_SECRET is set
Compose files unified:
- docker-compose.yml, docker-compose.enterprise-clean.yml,
docker-compose.e2e.yml — all use AUTH_SECRET_KEY
- build.sh generated compose now passes AUTH_SECRET_KEY + ENCRYPTION_KEY
to backend
Env examples unified and completed:
- .env.example — comprehensive template, all compose vars
- .env.enterprise-clean.example — production template
- backend/.env.example — backend-only run
- docker/.env.agent.example — agent-only run
- NEW: .env.current.example, .env.master.example,
.env.e2e.example, frontend/.env.example
Tests aligned:
- conftest sets AUTH_SECRET_KEY (canonical value matched across test files)
- test mocks use canonical name
- 1176 passed, 0 failed
2026-07-06 14:24:17 +03:00
a5b7adb61c
refactor(agent): lightweight JWT decoder — JWT_SECTRET first, AUTH_SECTRET_KEY fallback
...
Replaced src.core.auth.jwt.decode_token import with local _jwt_decoder.py:
- Tries JWT_SECTRET first, falls back to AUTH_SECTRET_KEY (avoids key mismatch
when both env vars are set in different test files)
- verify_aud disabled — backend tokens have audience; agent ignores
- No auth DB dependency — removed AUTH_DATABASE_URL requirement from agent
Cleanup:
- Removed src/core/auth/ from agent Dockerfile COPY
- Removed AUTH_SECTRET_KEY, AUTH_DATABASE_URL from agent compose env
- conftest sets AUTH_SECTRET_KEY for test consistency
- Updated test patches to new import path
Tests: 1176 passed, 0 failed
2026-07-06 13:45:22 +03:00
590b09f587
refactor(agent): use local JWT decoder instead of src.core.auth.jwt
...
Replace src.core.auth.jwt import with lightweight _jwt_decoder.py that
uses jose.jwt directly with JWT_SECRET env var. Avoids pulling AuthConfig
→ AUTH_DATABASE_URL/SECRET_KEY validators → SQLAlchemy models into agent.
Removed: AUTH_SECRET_KEY, AUTH_DATABASE_URL from agent compose env.
Removed: src/core/auth/ copies from Dockerfile.agent COPY section.
Added: src/agent/_jwt_decoder.py — stateless JWT validation.
2026-07-06 13:37:08 +03:00
b2d5aa5bf8
fix(agent): add libpq5 system dep for psycopg v3 (langgraph checkpoint)
2026-07-06 12:23:35 +03:00
558171989d
fix(agent): pass AUTH_DATABASE_URL to agent container, auth config requires it
2026-07-06 12:19:14 +03:00
468bdb9000
fix(agent): resolve missing imports — jose, auth config, AUTH_SECRET_KEY
...
- Added python-jose[cryptography] to requirements-agent.txt
- Made sqlalchemy.orm.Session and TokenBlacklist imports lazy in jwt.py
so decode_token works without pulling ORM models into agent image
- Added src/core/auth/__init__.py, config.py, jwt.py to agent Dockerfile COPY
- Added AUTH_SECRET_KEY env var to agent compose (reads from JWT_SECRET)
2026-07-06 12:13:26 +03:00
627b75497c
docs(adr): document agent source copy strategy and logger.py dependency
2026-07-06 11:35:57 +03:00
9f9ac07cac
fix(agent): copy src/core/logger.py and ws_log_handler.py into agent image
...
run.py imports from src.core.logger which requires logger.py and its
dependency ws_log_handler.py. Previously only cot_logger.py was copied.
2026-07-06 11:35:12 +03:00
628805503b
refactor(docker): split requirements by role, slim enterprise bundle by default
...
Split requirements.txt into role-specific files:
- requirements-backend.txt — FastAPI API runtime
- requirements-agent.txt — Gradio/LangGraph agent (no embeddings)
- requirements-embeddings.txt — opt-in sentence-transformers
Default enterprise bundle (./build.sh bundle) builds slim agent without
sentence-transformers/torch (~500 MB saved). Embeddings opt-in via
./build.sh bundle:embeddings <tag>. Embedding router degrades gracefully
when package is absent (ImportError catch in _embedding_router.py).
Updated Dockerfiles:
- backend.Dockerfile → requirements-backend.txt
- Dockerfile.agent → requirements-agent.txt + WITH_EMBEDDINGS build arg
- all-in-one.Dockerfile → requirements-backend.txt
Added embeddings variant to build.sh with full manifest/digest output.
Hardened .dockerignore (cache, report, model, archive patterns).
Other branch work: align git route mocks with async services, remove
legacy agent test files superseded by contract tests.
2026-07-06 10:06:07 +03:00
1586c31d9b
chore: причесать .env.example — удалить дубликаты и мёртвые vars
...
- backend/.env.enterprise-clean.example — удалён (точная копия root)
- .env.enterprise-clean.example — очищен от мёртвых OPENAI_API_KEY,
ANTHROPIC_API_KEY, добавлен пример Fernet-ключа
- docker/.env.agent.example — переписан под новую архитектуру:
убраны LLM_*, JWT_SECRET (больше не читаются агентом),
добавлен AUTH_SECRET_KEY
2026-07-06 08:55:11 +03:00
57b7ee05e0
test(git): align route mocks with async services
2026-07-06 01:23:20 +03:00
1e56416c9f
test(backend): update legacy agent and auth expectations
2026-07-06 01:23:09 +03:00
28cd141e76
feat(reports): add task status settings and tests
2026-07-06 01:22:39 +03:00
99bbdb4398
docs(agent-chat): update 035 verification traceability
2026-07-06 01:22:07 +03:00
082d6af3ab
test(agent-chat): audit guardrail and error handling
2026-07-06 01:20:12 +03:00
49e4ac0fe2
refactor(frontend): integrate compact filters into reports page
...
- Rename page title "Отчеты задач" → "Центр статусов"
- Remove standalone quick-status filter block (moved to FilterBar)
- Remove inline filter/sort/time-range HTML (replaced by FilterBar)
- Replace statusTotal() with quickStatusCounts
- Pass quickStatusCounts and onQuickStatusToggle to FilterBar
- Error recovery button text "Повторить" → "Повторить загрузку"
2026-07-05 15:50:51 +03:00
86ac209615
feat(frontend): compact FilterBar with disclosure toggle and quick pills
...
- Quick status pills (Упавшие/В работе/Успешные) always visible
- Search input with flex spacer in same row
- Explicit "Фильтры" disclosure button with filter icon, badge count, chevron
- aria-expanded / aria-controls for accessibility
- Expandable panel: type, status, time range, sort controls
- Remove bottom status bar ("Показано X из Y", "Активно фильтров")
- Update tests for expanded state and quick pill interactions
2026-07-05 15:50:46 +03:00
95308273b3
refactor(frontend): replace raw buttons with /ui/Button in SummaryPanel
...
- Replace raw <button> with <Button variant="ghost">
- Dim zero-count badges with opacity-50
- Add transition-colors duration-300 to count numbers
- Replace inline reconnect link with themed <Button>
2026-07-05 15:50:41 +03:00
c75017f1e4
fix(frontend): prevent TaskList horizontal overflow
...
- Add min-w-0 overflow-hidden to TaskList shell container
- Add filtered_empty UX state to contract
- Update layout contract test to verify TaskList source directly
- Add transition-colors to status badges
2026-07-05 15:50:36 +03:00
147d711657
feat(agent-chat): complete context guardrail event coverage
2026-07-05 14:14:42 +03:00
45e781fb74
chore: commit remaining working changes
...
Backend:
- agent: confirmation, persistence, app, langgraph_setup updates
- routes: agent_superset_explore, environments, git helpers/operations
- services: git sync refactoring
- tests: git_status_route expanded
Frontend:
- Navbar: minor cleanup
- Profile: i18n (en/ru), page enhancements, integration tests
- New: _llm_params.py
2026-07-05 09:24:45 +03:00
b773a06d52
refactor(frontend): extract BulkReplaceModal.Model — state + FSM + API
...
- BulkReplaceModalModel.svelte.ts (170 LOC): 13 state atoms,
7-state FSM (closed→configuring→previewing→confirming→applying→applied),
3 API actions (handlePreview, handleApply, loadDictionaries),
1 derived (isLargeChange), @RATIONALE/@REJECTED
- BulkReplaceModal.svelte: 461→~310 LOC (INV_7 compliance),
delegates all state/FSM/API to model, retains modal chrome + previewAfter()
- Follows TopNavbar/TranslationRunResult pattern
2026-07-05 09:21:49 +03:00
a91023d83e
fix(frontend): fix Svelte prop shorthand in TranslationRunResult
...
{model.targetLanguages} is invalid shorthand — Svelte only supports
bare identifiers, not property access. Changed to explicit
targetLanguages={model.targetLanguages}
2026-07-05 09:17:33 +03:00
97660cb71f
refactor(frontend): extract TranslationRunResult.Model — state + API logic
...
- TranslationRunResultModel.svelte.ts (200 LOC): 16 $state atoms,
3 $derived projections, 5 API actions (loadData, handleRetry,
handleRetryInsert, loadMoreRecords, loadBatches), @RATIONALE/@REJECTED
- TranslationRunResult.svelte: 623→456 LOC (167 lines saved, -27%),
delegates all state/API to model, retains template markup and
DOM helpers (copyToClipboard)
- INV_1: 0 naked functions (all logic in model)
- Follows TopNavbar pattern — dense anchor, hierarchical ID,
BINDS_TO -> [TranslationRunResult.Model]
2026-07-05 09:16:32 +03:00
669d8185f3
refactor(frontend): add @RATIONALE/@REJECTED to 5 C:4 contracts
...
- ConfigTabForm: rationale for single-tab vs wizard; rejected multi-step
- ValidationTaskForm: rationale for multi-step wizard; rejected flat form,
tabs, dynamic schema generation
- GitWorkspacePanel: rationale for IntersectionObserver lazy diff chunking;
rejected virtual scroll, server pagination, Web Worker
- Git.ManagerModel: rationale for model-first (cross-operation invariants,
L1 testability); existing @REJECTED preserved
- AgentChat.Component: rationale for reusable component vs inline;
rejected Web Component, iframe
2026-07-05 09:00:08 +03:00
deed06fada
refactor(frontend): remove @PURPOSE duplicates, merge into @BRIEF (INV_4)
...
- ProviderConfig: remove duplicate @LAYER/@PURPOSE/@UX_STATE block
- SemanticLayerReview: generic @BRIEF → detailed @PURPOSE text,
remove duplicate @LAYER/@SEMANTICS
- ExecutionMappingReview: same — generic @BRIEF → @PURPOSE detail
- TaskRunner: remove duplicate @SEMANTICS/@PURPOSE/@LAYER block
- ValidationFindingsPanel: generic @BRIEF → @PURPOSE detail,
remove duplicate @LAYER/@SEMANTICS
All: INV_4 compliance — single source of truth for contract metadata
in #region HTML comment, no scattered duplicates
2026-07-05 08:54:38 +03:00
008a8a92a5
refactor(frontend): clean metadata on AssistantChatPanel + TaskDrawer
...
- AssistantChatPanel: C:3→C:4 (33 side-effecting functions), remove JSDoc
duplicates (INV_4), add @PRE/@POST/@SIDE_EFFECT/@DATA_CONTRACT/@RATIONALE/@REJECTED
- TaskDrawer: remove JSDoc duplicates (INV_4), replace 4x @PURPOSE
JSDoc blocks with @BRIEF/@PRE/@POST in function contracts (INV_4),
add @RATIONALE/@REJECTED
- Both: consolidate all metadata in HTML comment #region, remove
scattered JSDoc in <script>
2026-07-05 08:53:17 +03:00
bc3e288d0b
refactor(frontend): extract TopNavbar.Model — search logic, semantic compliance
...
- Extract TopNavbarModel.svelte.ts (303 LOC) — search state, debounce,
API aggregation, drawer preference hydration
- TopNavbar.svelte: 605→389 LOC (INV_7 compliance)
- Remove duplicate JSDoc metadata (INV_4)
- Add @RATIONALE, @REJECTED, @PRE, @POST, @SIDE_EFFECT, @DATA_CONTRACT
- Replace raw Tailwind (from-sky-500/via-cyan-500/to-indigo-600 →
from-brand-gradient-from/via-brand-gradient-via/to-brand-gradient-to)
- Replace raw Tailwind focus:ring-sky-200 → focus:ring-primary-ring-light
- Fix hardcoded i18n 'Ассистент' → $t.assistant.assistant
- Add primary.ring-light token to tailwind.config.js
- Replace any types with concrete interfaces (DashboardSearchResult, etc.)
- 16 naked functions → 0 (INV_1)
2026-07-05 08:50:43 +03:00
ff60865183
feat(agent-chat): 035-agent-chat-context — контекст, guardrails, tools, database discovery
...
== User stories ==
US1: Контекст с дашборда/датасета → /agent с URL params
US2: Guardrails card — env badge, 7 risk tones, countdown, permission_denied
US3: Tools optimization — retry, timeout, summarise, RBAC + context affinity
== Backend ==
- _context.py (NEW): UIContext validation (7 checks)
- _tool_filter.py (NEW): RBAC + context affinity pipeline
- _confirmation.py: build_confirmation_contract_v2, permission_denied_payload
- tools.py: superset_list_databases, retry/summarise/timeout wrappers
- app.py: _inject_uicontext, _inject_env_id_into_tools, database
prefetch в runtime context
- _persistence.py: prefetch_databases()
- agent_superset_explore.py: GET /databases endpoint
- _llm_async_http.py, _persistence.py: fix double /v1 в LLM URL
(LM Studio Unexpected endpoint)
== Frontend ==
- AgentChatModel.svelte.ts: 5 atoms, 3 actions, countdown, context
- AgentChat.svelte: production banner, process steps, debug panel
- ConfirmationCard.svelte: 7 risk tones, permission_denied, countdown
- ToolCallCard.svelte: retrying/timeout/cancelled states
- StreamProcessor.svelte.ts: tool_retry, timeout, permission_denied
- TopNavbar: sparkles icon + Ассистент
- sidebarNavigation: AI section
- DashboardHeader, datasets/+page: contextual AI buttons
- Icon: sparkles, brain, cpu icons
- tailwind: assistant category colors
- i18n: en/ru nav keys
== Tests ==
- 159 backend agent tests (+16 US3: retry, timeout, summarise, contracts)
- 2544 frontend tests (+11 model + component tests)
- 15 JSON fixtures (10 API + 5 model)
== Specs ==
- specs/035-agent-chat-context/: spec, UX, plan, tasks, research,
data-model, contracts, quickstart, traceability, fixtures, checklists
Closes #035
2026-07-04 22:47:17 +03:00
4c5fde8b5c
chore(kilo): sync OpenCode config into Kilo format
2026-07-04 22:43:17 +03:00
1f502c9785
docs(readme): add SSL/TLS configuration section with passphrase and PKCS#12 options
2026-07-04 17:10:04 +03:00
1c6b3d6e8a
feat(ssl): add passphrase-protected key and PKCS#12 support in nginx entrypoint
...
- docker/frontend.entrypoint.sh: add 3 new functions:
- extract_p12_if_needed — openssl pkcs12 extraction to .crt+.key
- resolve_ssl_passphrase — read SSL_KEY_PASSPHRASE env var
- decrypt_key_if_needed — openssl rsa decryption before nginx start
- Pipeline: install CA -> extract p12 -> get passphrase -> select config -> decrypt key -> start nginx
- Crash-early: encrypted key without SSL_KEY_PASSPHRASE exits entrypoint
- docker-compose.enterprise-clean.yml: add SSL_KEY_PASSPHRASE to frontend env
- .env.enterprise-clean.example: document SSL_KEY_PASSPHRASE usage
- build.sh: add SSL_KEY_PASSPHRASE to generated deploy compose
2026-07-04 17:05:51 +03:00
047aff41d9
Task Status Center: save progress
2026-07-04 15:34:02 +03:00
61f3e6db75
feat: Git manager UI — панель управления Git + HelpTooltip + ReviewToggle
...
- GitManager: переработан в GitWorkspacePanel с вкладками
- Добавлен GitLifecycleHeader с быстрыми действиями
- RepositoryDashboardGrid: поддержка ReviewToggle, badges, env filter
- HelpTooltip: универсальный компонент подсказок с тестами
- GitManagerModel: доработаны экшены, добавлен isReady, loadDefaultBranch
- Локализация en/ru для Git UI
- tailwind: добавлен animation-delay-200
- ConfirmDialog: a11y-атрибуты для кнопок
2026-07-04 15:01:45 +03:00
0237e2f7e8
chore: добавить пример Fernet-ключа в .env.example
...
- Сгенерированный пример ENCRYPTION_KEY с командой для генерации
- Пометка 'сгенерируйте свой для прода'
2026-07-04 15:01:23 +03:00
b78493aeb7
chore: добавить примеры значений в .env.example
...
- backend/.env.example — пример DATABASE_URL с комментарием
- root/.env.example — заполнены примеры для всех переменных
(AUTH_SECRET_KEY, DATABASE_URL, INITIAL_ADMIN_PASSWORD, POSTGRES_PASSWORD)
2026-07-04 14:59:39 +03:00
ec9bc76a37
cleanup: убрать мёртвые env-переменные, консолидировать чтение в agent/_config.py
...
Удалены из кода:
- JWT_SECRET — мёртвая (decode_token использует AUTH_SECRET_KEY)
- SESSION_SECRET_KEY — заменён на прямой AUTH_SECRET_KEY
- POSTGRES_URL — deprecated fallback, удалён из database.py и reencrypt.py
Консолидировано чтение env-переменных agent-модуля:
- Создан agent/_config.py — единый модуль для FASTAPI_URL,
SERVICE_JWT, GRADIO_*, STORAGE_ROOT, AGENT_* (9 констант)
- Все agent/*.py импортируют из _config вместо разрозненных os.getenv
Удалены or-дефолты (безопасность):
- agent/langgraph_setup.py — удалён hardcoded DB URL postgres:postgres
- agent/langgraph_setup.py — удалены fallback API URL и model name
- scripts/reencrypt.py — удалён hardcoded DB URL postgres:postgres
- plugins/llm_analysis/service.py — удалены or-дефолты URL/app name
.env.example — минимализация:
- backend/.env.example: только 4 обязательные переменные
- root/.env.example: обязательные + docker + SSO/админ
Обновлены тесты (139 passed)
2026-07-04 14:58:43 +03:00
a99c1d6d01
Improve agent UX and spec sync
2026-07-03 16:47:10 +03:00
500491c281
test(tls): add encrypted (passphrase-protected) private key integration tests
...
- ca_chain fixture: add server_key_encrypted (BestAvailableEncryption) +
server_key_passphrase to returned dict; existing fields unchanged
- TestEncryptedPrivateKey: 6 new tests covering
- ssl.SSLContext.load_cert_chain() with correct/wrong/missing passphrase
- asyncio SSL server end-to-end with encrypted key + full chain trust
- openssl rsa -check with wrong/missing passphrase (CLI validation)
- All 6 tests pass; 14/14 in test_superset_tls_custom_ca.py
2026-07-03 16:46:12 +03:00
89b340c64f
fix(agent): debug panel copyDebugInfo — full state snapshot (was 9 fields, now all model atoms)
...
Previously copyDebugInfo() only exported 9 hand-picked fields
(conversation_id, thread_id, connection/streaming state, user, env,
message count, truncated tool calls, error). The debug panel was
missing LLM health (status, retry, banner), confirmation/HITL state
(pending_tool, args, risk), queue position, message previews, full
tool call objects, and UI flags (sidebar, debug panel, cancelled).
Now:
- Full model snapshot with all fields
- active_tool_calls_full — complete ToolCall objects
- last_message_preview — first 200 chars of last message
- LLM status, banner dismiss, retry countdown
- HITL confirmation state (pending_tool_name, args, risk, level)
- Conversations count, queue position, user_cancelled flag
- Visual debug panel grid: 5-column layout with new rows for
LLM health, confirmation, queue, sidebar, convs_count
2026-07-03 15:35:22 +03:00
53eb2b1cca
Improve reports UI and task drawer UX
2026-07-03 14:50:05 +03:00