From f24200d52a8a597511ac9770fd4934a8b3295c35 Mon Sep 17 00:00:00 2001 From: busya Date: Sun, 1 Mar 2026 12:13:19 +0300 Subject: [PATCH] git list refactor --- .ai/MODULE_MAP.md | 124 +- .ai/PROJECT_MAP.md | 369 +- .../routes/__tests__/test_assistant_api.py | 56 +- .../routes/__tests__/test_git_status_route.py | 198 + backend/src/api/routes/assistant.py | 109 +- backend/src/api/routes/dashboards.py | 86 +- backend/src/api/routes/git.py | 170 +- backend/src/api/routes/git_schemas.py | 17 +- backend/src/core/superset_client.py | 94 + backend/src/services/git_service.py | 62 +- backend/src/services/resource_service.py | 61 + .../components/RepositoryDashboardGrid.svelte | 613 ++ frontend/src/components/git/GitManager.svelte | 45 +- frontend/src/lib/api.js | 392 +- frontend/src/lib/api/assistant.js | 10 + .../assistant/AssistantChatPanel.svelte | 66 +- frontend/src/lib/i18n/locales/en.json | 25 +- frontend/src/lib/i18n/locales/ru.json | 32 +- frontend/src/lib/ui/Icon.svelte | 40 +- frontend/src/routes/dashboards/+page.svelte | 175 +- frontend/src/routes/git/+page.svelte | 4 +- .../src/routes/storage/backups/+page.svelte | 38 +- .../src/routes/storage/repos/+page.svelte | 6 +- .../src/routes/tools/storage/+page.svelte | 101 +- frontend/src/services/gitService.js | 17 +- semantics/semantic_map.json | 9582 ++++++++++++++--- 26 files changed, 10313 insertions(+), 2179 deletions(-) create mode 100644 backend/src/api/routes/__tests__/test_git_status_route.py create mode 100644 frontend/src/components/RepositoryDashboardGrid.svelte diff --git a/.ai/MODULE_MAP.md b/.ai/MODULE_MAP.md index af0d4ff..27610b4 100644 --- a/.ai/MODULE_MAP.md +++ b/.ai/MODULE_MAP.md @@ -2,12 +2,12 @@ > High-level module structure for AI Context. Generated automatically. -**Generated:** 2026-02-27T15:09:17.269938 +**Generated:** 2026-03-01T12:09:39.463912 ## Summary -- **Total Modules:** 78 -- **Total Entities:** 1927 +- **Total Modules:** 80 +- **Total Entities:** 2080 ## Module Hierarchy @@ -54,9 +54,9 @@ ### πŸ“ `routes/` - πŸ—οΈ **Layers:** API, UI (API) - - πŸ“Š **Tiers:** CRITICAL: 3, STANDARD: 197, TRIVIAL: 6 + - πŸ“Š **Tiers:** CRITICAL: 3, STANDARD: 205, TRIVIAL: 7 - πŸ“„ **Files:** 17 - - πŸ“¦ **Entities:** 206 + - πŸ“¦ **Entities:** 215 **Key Entities:** @@ -92,9 +92,9 @@ ### πŸ“ `__tests__/` - πŸ—οΈ **Layers:** API, Domain (Tests), UI (API Tests) - - πŸ“Š **Tiers:** STANDARD: 51, TRIVIAL: 104 - - πŸ“„ **Files:** 8 - - πŸ“¦ **Entities:** 155 + - πŸ“Š **Tiers:** STANDARD: 61, TRIVIAL: 121 + - πŸ“„ **Files:** 9 + - πŸ“¦ **Entities:** 182 **Key Entities:** @@ -126,9 +126,9 @@ ### πŸ“ `core/` - πŸ—οΈ **Layers:** Core - - πŸ“Š **Tiers:** CRITICAL: 2, STANDARD: 125, TRIVIAL: 8 + - πŸ“Š **Tiers:** CRITICAL: 2, STANDARD: 131, TRIVIAL: 8 - πŸ“„ **Files:** 10 - - πŸ“¦ **Entities:** 135 + - πŸ“¦ **Entities:** 141 **Key Entities:** @@ -219,6 +219,36 @@ - πŸ“¦ **test_logger** (Module) - Unit tests for logger module + ### πŸ“ `migration/` + + - πŸ—οΈ **Layers:** Core + - πŸ“Š **Tiers:** STANDARD: 20, TRIVIAL: 1 + - πŸ“„ **Files:** 4 + - πŸ“¦ **Entities:** 21 + + **Key Entities:** + + - β„‚ **MigrationArchiveParser** (Class) + - Extract normalized dashboards/charts/datasets metadata from ... + - β„‚ **MigrationDryRunService** (Class) + - Build deterministic diff/risk payload for migration pre-flig... + - πŸ“¦ **backend.src.core.migration.__init__** (Module) `[TRIVIAL]` + - Namespace package for migration pre-flight orchestration com... + - πŸ“¦ **backend.src.core.migration.archive_parser** (Module) + - Parse Superset export ZIP archives into normalized object ca... + - πŸ“¦ **backend.src.core.migration.dry_run_orchestrator** (Module) + - Compute pre-flight migration diff and risk scoring without a... + - πŸ“¦ **backend.src.core.migration.risk_assessor** (Module) + - Risk evaluation helpers for migration pre-flight reporting. + + **Dependencies:** + + - πŸ”— DEPENDS_ON -> backend.src.core.logger + - πŸ”— DEPENDS_ON -> backend.src.core.migration.archive_parser + - πŸ”— DEPENDS_ON -> backend.src.core.migration.risk_assessor + - πŸ”— DEPENDS_ON -> backend.src.core.migration_engine + - πŸ”— DEPENDS_ON -> backend.src.core.superset_client + ### πŸ“ `task_manager/` - πŸ—οΈ **Layers:** Core @@ -507,9 +537,9 @@ ### πŸ“ `services/` - πŸ—οΈ **Layers:** Core, Domain, Service - - πŸ“Š **Tiers:** CRITICAL: 1, STANDARD: 58, TRIVIAL: 5 + - πŸ“Š **Tiers:** CRITICAL: 1, STANDARD: 62, TRIVIAL: 6 - πŸ“„ **Files:** 7 - - πŸ“¦ **Entities:** 64 + - πŸ“¦ **Entities:** 69 **Key Entities:** @@ -650,15 +680,31 @@ - πŸ“¦ **test_defensive_guards** (Module) `[TRIVIAL]` - Auto-generated module for backend/tests/core/test_defensive_... + ### πŸ“ `migration/` + + - πŸ—οΈ **Layers:** Domain + - πŸ“Š **Tiers:** STANDARD: 2, TRIVIAL: 4 + - πŸ“„ **Files:** 2 + - πŸ“¦ **Entities:** 6 + + **Key Entities:** + + - πŸ“¦ **backend.tests.core.migration.test_archive_parser** (Module) + - Unit tests for MigrationArchiveParser ZIP extraction contrac... + - πŸ“¦ **backend.tests.core.migration.test_dry_run_orchestrator** (Module) + - Unit tests for MigrationDryRunService diff and risk computat... + ### πŸ“ `components/` - πŸ—οΈ **Layers:** Component, Feature, UI, UI -->, Unknown - - πŸ“Š **Tiers:** CRITICAL: 1, STANDARD: 49, TRIVIAL: 4 - - πŸ“„ **Files:** 13 - - πŸ“¦ **Entities:** 54 + - πŸ“Š **Tiers:** CRITICAL: 1, STANDARD: 68, TRIVIAL: 4 + - πŸ“„ **Files:** 14 + - πŸ“¦ **Entities:** 73 **Key Entities:** + - 🧩 **DashboardGrid** (Component) + - Displays a grid of dashboards with selection and pagination. - 🧩 **DashboardGrid** (Component) - Displays a grid of dashboards with selection and pagination. - 🧩 **DynamicForm** (Component) @@ -677,8 +723,6 @@ - A modal component to prompt the user for database passwords ... - 🧩 **TaskHistory** (Component) - Displays a list of recent tasks with their status and allows... - - 🧩 **TaskList** (Component) - - Displays a list of tasks with their status and execution det... ### πŸ“ `__tests__/` @@ -707,9 +751,9 @@ ### πŸ“ `git/` - πŸ—οΈ **Layers:** Component - - πŸ“Š **Tiers:** STANDARD: 26 + - πŸ“Š **Tiers:** STANDARD: 28 - πŸ“„ **Files:** 6 - - πŸ“¦ **Entities:** 26 + - πŸ“¦ **Entities:** 28 **Key Entities:** @@ -817,9 +861,9 @@ ### πŸ“ `lib/` - πŸ—οΈ **Layers:** Infra, Infra-API, UI, UI-State - - πŸ“Š **Tiers:** STANDARD: 23, TRIVIAL: 3 + - πŸ“Š **Tiers:** STANDARD: 24, TRIVIAL: 3 - πŸ“„ **Files:** 5 - - πŸ“¦ **Entities:** 26 + - πŸ“¦ **Entities:** 27 **Key Entities:** @@ -837,9 +881,9 @@ ### πŸ“ `api/` - πŸ—οΈ **Layers:** Infra, Infra-API - - πŸ“Š **Tiers:** CRITICAL: 1, STANDARD: 10 + - πŸ“Š **Tiers:** CRITICAL: 1, STANDARD: 11 - πŸ“„ **Files:** 2 - - πŸ“¦ **Entities:** 11 + - πŸ“¦ **Entities:** 12 **Key Entities:** @@ -886,9 +930,9 @@ ### πŸ“ `assistant/` - πŸ—οΈ **Layers:** UI, Unknown - - πŸ“Š **Tiers:** CRITICAL: 1, STANDARD: 12, TRIVIAL: 5 + - πŸ“Š **Tiers:** CRITICAL: 1, STANDARD: 13, TRIVIAL: 5 - πŸ“„ **Files:** 1 - - πŸ“¦ **Entities:** 18 + - πŸ“¦ **Entities:** 19 **Key Entities:** @@ -912,9 +956,9 @@ ### πŸ“ `layout/` - πŸ—οΈ **Layers:** UI, Unknown - - πŸ“Š **Tiers:** CRITICAL: 3, STANDARD: 5, TRIVIAL: 46 + - πŸ“Š **Tiers:** CRITICAL: 3, STANDARD: 5, TRIVIAL: 48 - πŸ“„ **Files:** 4 - - πŸ“¦ **Entities:** 54 + - πŸ“¦ **Entities:** 56 **Key Entities:** @@ -1216,9 +1260,9 @@ ### πŸ“ `dashboards/` - πŸ—οΈ **Layers:** UI, Unknown - - πŸ“Š **Tiers:** CRITICAL: 1, TRIVIAL: 35 + - πŸ“Š **Tiers:** CRITICAL: 1, STANDARD: 23, TRIVIAL: 60 - πŸ“„ **Files:** 1 - - πŸ“¦ **Entities:** 36 + - πŸ“¦ **Entities:** 84 **Key Entities:** @@ -1288,9 +1332,9 @@ ### πŸ“ `migration/` - πŸ—οΈ **Layers:** Page - - πŸ“Š **Tiers:** STANDARD: 10 + - πŸ“Š **Tiers:** STANDARD: 11 - πŸ“„ **Files:** 1 - - πŸ“¦ **Entities:** 10 + - πŸ“¦ **Entities:** 11 **Key Entities:** @@ -1327,9 +1371,9 @@ ### πŸ“ `[taskId]/` - πŸ—οΈ **Layers:** Unknown - - πŸ“Š **Tiers:** TRIVIAL: 8 + - πŸ“Š **Tiers:** TRIVIAL: 11 - πŸ“„ **Files:** 1 - - πŸ“¦ **Entities:** 8 + - πŸ“¦ **Entities:** 11 **Key Entities:** @@ -1412,14 +1456,14 @@ ### πŸ“ `storage/` - πŸ—οΈ **Layers:** UI - - πŸ“Š **Tiers:** STANDARD: 5 + - πŸ“Š **Tiers:** STANDARD: 6 - πŸ“„ **Files:** 1 - - πŸ“¦ **Entities:** 5 + - πŸ“¦ **Entities:** 6 **Key Entities:** - 🧩 **StoragePage** (Component) - - Main page for file storage management. + - Main page for unified file storage management. ### πŸ“ `services/` @@ -1539,6 +1583,12 @@ graph TD auth-->|USES|backend auth-->|USES|backend auth-->|USES|backend + migration-->|DEPENDS_ON|backend + migration-->|DEPENDS_ON|backend + migration-->|DEPENDS_ON|backend + migration-->|DEPENDS_ON|backend + migration-->|DEPENDS_ON|backend + migration-->|USED_BY|backend utils-->|DEPENDS_ON|backend utils-->|DEPENDS_ON|backend utils-->|DEPENDS_ON|backend @@ -1587,6 +1637,8 @@ graph TD tests-->|TESTS|backend core-->|VERIFIES|backend core-->|VERIFIES|backend + migration-->|VERIFIES|backend + migration-->|VERIFIES|backend __tests__-->|VERIFIES|components __tests__-->|VERIFIES|lib reports-->|DEPENDS_ON|lib diff --git a/.ai/PROJECT_MAP.md b/.ai/PROJECT_MAP.md index 873d655..dfdc8d0 100644 --- a/.ai/PROJECT_MAP.md +++ b/.ai/PROJECT_MAP.md @@ -165,6 +165,8 @@ - πŸ“ Creates a normalized Error object for failed API responses. - Ζ’ **notifyApiError** (`Function`) - πŸ“ Shows toast for API errors with explicit handling of critical statuses. + - Ζ’ **shouldSuppressApiErrorToast** (`Function`) + - πŸ“ Avoid noisy toasts for expected non-critical API failures. - Ζ’ **getWsUrl** (`Function`) - πŸ“ Returns the WebSocket URL for a specific task, with fallback logic. - Ζ’ **getAuthHeaders** (`Function`) @@ -187,6 +189,7 @@ - πŸ—„οΈ **authStore** (`Store`) - πŸ“ Manages the global authentication state on the frontend. - πŸ—οΈ Layer: Feature + - πŸ”— BINDS_TO -> `Navbar, ProtectedRoute` - πŸ“¦ **AuthState** (`Interface`) - πŸ“ Defines the structure of the authentication state. - Ζ’ **createAuthStore** (`Function`) @@ -208,6 +211,7 @@ - πŸ“ Control assistant chat panel visibility and active conversation binding. - πŸ—οΈ Layer: UI - πŸ”’ Invariant: conversationId persists while panel toggles unless explicitly reset. + - πŸ”— BINDS_TO -> `AssistantChatPanel` - Ζ’ **toggleAssistantChat** (`Function`) - πŸ“ Toggle assistant panel visibility. - Ζ’ **openAssistantChat** (`Function`) @@ -344,6 +348,8 @@ - πŸ“ Retrieve paginated assistant conversation history. - Ζ’ **getAssistantConversations** (`Function`) - πŸ“ Retrieve paginated conversation list for assistant sidebar/history switcher. + - Ζ’ **deleteAssistantConversation** (`Function`) + - πŸ“ Soft-delete or hard-delete a conversation. - πŸ“¦ **frontend.src.lib.api.__tests__.reports_api** (`Module`) - πŸ“ Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers. - πŸ—οΈ Layer: Infra (Tests) @@ -415,6 +421,8 @@ - πŸ“ Load current conversation history when panel becomes visible. - Ζ’ **loadConversations** (`Function`) - πŸ“ Load paginated conversation summaries for quick switching UI. + - Ζ’ **removeConversation** (`Function`) + - πŸ“ Removes a conversation from the list and deletes it from the backend. - Ζ’ **loadOlderMessages** (`Function`) - πŸ“ Lazy-load older messages for active conversation when user scrolls to top. - Ζ’ **appendLocalUserMessage** (`Function`) @@ -658,6 +666,10 @@ - πŸ“ Auto-detected function (orphan) - Ζ’ **resolveEnvironmentId** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) + - Ζ’ **normalizeSupersetBaseUrl** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **resolveSupersetDashboardUrl** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - Ζ’ **loadActiveTaskDetails** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) - Ζ’ **extractPrimaryDashboardId** (`Function`) `[TRIVIAL]` @@ -786,6 +798,12 @@ - πŸ“ Auto-detected function (orphan) - Ζ’ **openTaskDetails** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) + - Ζ’ **cleanupScreenshotBlobUrls** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **loadScreenshotBlobUrls** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **openScreenshot** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - 🧩 **LoginPage** (`Component`) - πŸ“ Provides the user interface for local and ADFS authentication. - πŸ—οΈ Layer: UI @@ -810,11 +828,67 @@ - πŸ“ Dashboard Hub - Central hub for managing dashboards with Git status and task actions - πŸ—οΈ Layer: UI - πŸ”’ Invariant: Always shows dashboards for the active environment from context store + - Ζ’ **DashboardHub.normalizeTaskStatus** (`Function`) + - πŸ“ Normalize raw task status to stable lowercase token for UI. + - Ζ’ **DashboardHub.normalizeValidationStatus** (`Function`) + - πŸ“ Normalize validation status to pass/fail/warn/unknown. + - Ζ’ **DashboardHub.getValidationBadgeClass** (`Function`) + - πŸ“ Map validation level to badge class tuple. + - Ζ’ **DashboardHub.getValidationLabel** (`Function`) + - πŸ“ Map normalized validation level to compact UI label. + - Ζ’ **DashboardHub.normalizeOwners** (`Function`) + - πŸ“ Normalize owners payload to unique non-empty display labels. + - Ζ’ **DashboardHub.loadDashboards** (`Function`) + - πŸ“ Load full dashboard dataset for current environment and hydrate grid projection. + - Ζ’ **DashboardHub.formatDate** (`Function`) + - πŸ“ Convert ISO timestamp to locale date string. + - Ζ’ **DashboardHub.getGitSummaryLabel** (`Function`) + - πŸ“ Compute stable text label for git state column. + - Ζ’ **DashboardHub.getLlmSummaryLabel** (`Function`) + - πŸ“ Compute normalized LLM validation summary label. + - Ζ’ **DashboardHub.getColumnCellValue** (`Function`) + - πŸ“ Resolve comparable/filterable display value for any grid column. + - Ζ’ **DashboardHub.getFilterOptions** (`Function`) + - πŸ“ Build unique sorted value list for a column filter dropdown. + - Ζ’ **DashboardHub.getVisibleFilterOptions** (`Function`) + - πŸ“ Apply in-dropdown search over full filter options. + - Ζ’ **DashboardHub.toggleFilterDropdown** (`Function`) + - πŸ“ Toggle active column filter popover. + - Ζ’ **DashboardHub.toggleFilterValue** (`Function`) + - πŸ“ Add/remove specific filter value and reapply projection. + - Ζ’ **DashboardHub.clearColumnFilter** (`Function`) + - πŸ“ Reset selected values for one column. + - Ζ’ **DashboardHub.selectAllColumnFilterValues** (`Function`) + - πŸ“ Select all currently visible values in filter popover. + - Ζ’ **DashboardHub.updateColumnFilterSearch** (`Function`) + - πŸ“ Update local search token for one filter popover. + - Ζ’ **DashboardHub.hasColumnFilter** (`Function`) + - πŸ“ Determine if column has active selected values. + - Ζ’ **DashboardHub.doesDashboardPassColumnFilters** (`Function`) + - πŸ“ Evaluate dashboard row against all active column filters. + - Ζ’ **DashboardHub.getSortValue** (`Function`) + - πŸ“ Compute stable comparable sort key for chosen column. + - Ζ’ **DashboardHub.handleSort** (`Function`) + - πŸ“ Toggle or switch sort order and reapply grid projection. + - Ζ’ **DashboardHub.getSortIndicator** (`Function`) + - πŸ“ Return visual indicator for active/inactive sort header. + - Ζ’ **DashboardHub.applyGridTransforms** (`Function`) + - πŸ“ Apply search + column filters + sort + pagination to grid data. - πŸ“¦ **+page** (`Module`) `[TRIVIAL]` - πŸ“ Auto-generated module for frontend/src/routes/dashboards/+page.svelte - πŸ—οΈ Layer: Unknown - Ζ’ **handleDocumentClick** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) + - Ζ’ **normalizeTaskStatus** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **normalizeValidationStatus** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **getValidationBadgeClass** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **getValidationLabel** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **normalizeOwners** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - Ζ’ **loadDashboards** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) - Ζ’ **handleSearch** (`Function`) `[TRIVIAL]` @@ -831,6 +905,40 @@ - πŸ“ Auto-detected function (orphan) - Ζ’ **handleSelectVisible** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) + - Ζ’ **formatDate** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **getGitSummaryLabel** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **getLlmSummaryLabel** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **getColumnCellValue** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **getFilterOptions** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **getVisibleFilterOptions** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **toggleFilterDropdown** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **toggleFilterValue** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **clearColumnFilter** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **selectAllColumnFilterValues** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **updateColumnFilterSearch** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **hasColumnFilter** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **doesDashboardPassColumnFilters** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **getSortValue** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **handleSort** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **getSortIndicator** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **applyGridTransforms** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - Ζ’ **toggleActionDropdown** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) - Ζ’ **closeActionDropdown** (`Function`) `[TRIVIAL]` @@ -847,6 +955,8 @@ - πŸ“ Auto-detected function (orphan) - Ζ’ **loadDbMappings** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) + - Ζ’ **calculateDryRun** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - Ζ’ **handleBulkMigrate** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) - Ζ’ **handleBulkBackup** (`Function`) `[TRIVIAL]` @@ -865,8 +975,12 @@ - πŸ“ Auto-detected function (orphan) - Ζ’ **updateDashboardGitState** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) + - Ζ’ **normalizeRepositoryStatusPayload** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - Ζ’ **refreshDashboardGitState** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) + - Ζ’ **hydrateVisibleGitStatuses** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - Ζ’ **handleGitInit** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) - Ζ’ **handleGitSync** (`Function`) `[TRIVIAL]` @@ -1008,6 +1122,8 @@ - πŸ“ Resumes a migration task with provided passwords. - Ζ’ **startMigration** (`Function`) - πŸ“ Starts the migration process. + - Ζ’ **startDryRun** (`Function`) + - πŸ“ Builds pre-flight diff and risk summary without applying migration. - 🧩 **DashboardSelectionSection** (`Component`) - 🧩 **MappingManagement** (`Component`) - πŸ“ Page for managing database mappings between environments. @@ -1023,9 +1139,9 @@ - Ζ’ **handleUpdate** (`Function`) - πŸ“ Saves a mapping to the backend. - 🧩 **StoragePage** (`Component`) - - πŸ“ Main page for file storage management. + - πŸ“ Main page for unified file storage management. - πŸ—οΈ Layer: UI - - πŸ”’ Invariant: Always displays tabs for Backups and Repositories. + - πŸ”’ Invariant: Always displays a unified storage view without category tabs. - ⬅️ READS_FROM `app` - ⬅️ READS_FROM `t` - ➑️ WRITES_TO `page` @@ -1037,6 +1153,8 @@ - πŸ“ Updates the current path and reloads files when navigating into a directory. - Ζ’ **navigateUp** (`Function`) - πŸ“ Navigates one level up in the directory structure. + - Ζ’ **updateUploadCategory** (`Function`) + - πŸ“ Keeps upload category aligned with the currently viewed top-level folder. - 🧩 **MapperPage** (`Component`) `[TRIVIAL]` - πŸ“ Page for the dataset column mapper tool. - πŸ—οΈ Layer: UI @@ -1231,6 +1349,46 @@ - πŸ“ Deletes a file or directory from storage. - Ζ’ **downloadFileUrl** (`Function`) - πŸ“ Returns the URL for downloading a file. +- 🧩 **DashboardGrid** (`Component`) + - πŸ“ Displays a grid of dashboards with selection and pagination. + - πŸ—οΈ Layer: Component + - πŸ”’ Invariant: Selected IDs must be a subset of available dashboards. + - ⚑ Events: selectionChanged + - ➑️ WRITES_TO `props` + - ➑️ WRITES_TO `state` + - ➑️ WRITES_TO `derived` + - Ζ’ **handleSort** (`Function`) + - πŸ“ Toggles sort direction or changes sort column. + - Ζ’ **handleSelectionChange** (`Function`) + - πŸ“ Handles individual checkbox changes. + - Ζ’ **handleSelectAll** (`Function`) + - πŸ“ Handles select all checkbox. + - Ζ’ **goToPage** (`Function`) + - πŸ“ Changes current page. + - Ζ’ **getRepositoryStatusToken** (`Function`) + - πŸ“ Returns normalized repository status token for a dashboard. + - Ζ’ **isRepositoryReady** (`Function`) + - πŸ“ Determines whether git actions can run for a dashboard. + - Ζ’ **invalidateRepositoryStatuses** (`Function`) + - πŸ“ Marks dashboard statuses as loading so they are refetched. + - Ζ’ **resolveRepositoryStatusToken** (`Function`) + - πŸ“ Converts git status payload into a stable UI status token. + - Ζ’ **loadRepositoryStatuses** (`Function`) + - πŸ“ Hydrates repository status map for dashboards in repository mode. + - Ζ’ **runBulkGitAction** (`Function`) + - πŸ“ Executes git action for selected dashboards with limited parallelism. + - Ζ’ **handleBulkSync** (`Function`) + - Ζ’ **handleBulkCommit** (`Function`) + - Ζ’ **handleBulkPull** (`Function`) + - Ζ’ **handleBulkPush** (`Function`) + - Ζ’ **handleManageSelected** (`Function`) + - πŸ“ Opens Git manager for exactly one selected dashboard. + - Ζ’ **getSortStatusValue** (`Function`) + - πŸ“ Returns sort value for status column based on mode. + - Ζ’ **getStatusLabel** (`Function`) + - πŸ“ Returns localized label for status column. + - Ζ’ **getStatusBadgeClass** (`Function`) + - πŸ“ Returns badge style for status column. - 🧩 **PasswordPrompt** (`Component`) - πŸ“ A modal component to prompt the user for database passwords when a migration task is paused. - πŸ—οΈ Layer: UI @@ -1605,6 +1763,10 @@ - πŸ“ Pushes local commits to the remote repository. - Ζ’ **handlePull** (`Function`) - πŸ“ Pulls changes from the remote repository. + - Ζ’ **closeModal** (`Function`) + - πŸ“ Π—Π°ΠΊΡ€Ρ‹Π²Π°Π΅Ρ‚ модальноС ΠΎΠΊΠ½ΠΎ управлСния Git. + - Ζ’ **handleBackdropClick** (`Function`) + - πŸ“ Π—Π°ΠΊΡ€Ρ‹Π²Π°Π΅Ρ‚ ΠΌΠΎΠ΄Π°Π»ΠΊΡƒ ΠΏΠΎ ΠΊΠ»ΠΈΠΊΡƒ Π½Π° ΠΏΠΎΠ΄Π»ΠΎΠΆΠΊΡƒ. - 🧩 **DocPreview** (`Component`) - πŸ“ UI component for previewing generated dataset documentation before saving. - πŸ—οΈ Layer: UI @@ -1817,14 +1979,26 @@ - πŸ“ Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Π±Π°Π·ΠΎΠ²Ρ‹Π΅ HTTP-Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡ‹Π΅ сСтСвым ΠΊΠ»ΠΈΠ΅Π½Ρ‚ΠΎΠΌ. - Ζ’ **get_dashboards** (`Function`) - πŸ“ ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ ΠΏΠΎΠ»Π½Ρ‹ΠΉ список Π΄Π°ΡˆΠ±ΠΎΡ€Π΄ΠΎΠ², автоматичСски обрабатывая ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΡŽ. + - Ζ’ **get_dashboards_page** (`Function`) + - πŸ“ Fetches a single dashboards page from Superset without iterating all pages. - Ζ’ **get_dashboards_summary** (`Function`) - πŸ“ Fetches dashboard metadata optimized for the grid. + - Ζ’ **get_dashboards_summary_page** (`Function`) + - πŸ“ Fetches one page of dashboard metadata optimized for the grid. + - Ζ’ **_extract_owner_labels** (`Function`) + - πŸ“ Normalize dashboard owners payload to stable display labels. + - Ζ’ **_extract_user_display** (`Function`) + - πŸ“ Normalize user payload to a stable display name. + - Ζ’ **_sanitize_user_text** (`Function`) + - πŸ“ Convert scalar value to non-empty user-facing text. - Ζ’ **get_dashboard** (`Function`) - πŸ“ Fetches a single dashboard by ID. - Ζ’ **get_chart** (`Function`) - πŸ“ Fetches a single chart by ID. - Ζ’ **get_dashboard_detail** (`Function`) - πŸ“ Fetches detailed dashboard information including related charts and datasets. + - Ζ’ **get_charts** (`Function`) + - πŸ“ Fetches all charts with pagination support. - Ζ’ **_extract_chart_ids_from_layout** (`Function`) - πŸ“ Traverses dashboard layout metadata and extracts chart IDs from common keys. - Ζ’ **export_dashboard** (`Function`) @@ -2342,6 +2516,59 @@ - πŸ“ Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated. - Ζ’ **reset_logger_state** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) +- πŸ“¦ **backend.src.core.migration.dry_run_orchestrator** (`Module`) + - πŸ“ Compute pre-flight migration diff and risk scoring without apply. + - πŸ—οΈ Layer: Core + - πŸ”’ Invariant: Dry run is informative only and must not mutate target environment. + - πŸ”— DEPENDS_ON -> `backend.src.core.superset_client` + - πŸ”— DEPENDS_ON -> `backend.src.core.migration_engine` + - πŸ”— DEPENDS_ON -> `backend.src.core.migration.archive_parser` + - πŸ”— DEPENDS_ON -> `backend.src.core.migration.risk_assessor` + - β„‚ **MigrationDryRunService** (`Class`) + - πŸ“ Build deterministic diff/risk payload for migration pre-flight. + - Ζ’ **__init__** (`Function`) + - πŸ“ Wire parser dependency for archive object extraction. + - Ζ’ **run** (`Function`) + - πŸ“ Execute full dry-run computation for selected dashboards. + - Ζ’ **_load_db_mapping** (`Function`) + - πŸ“ Resolve UUID mapping for optional DB config replacement. + - Ζ’ **_accumulate_objects** (`Function`) + - πŸ“ Merge extracted resources by UUID to avoid duplicates. + - Ζ’ **_index_by_uuid** (`Function`) + - πŸ“ Build UUID-index map for normalized resources. + - Ζ’ **_build_object_diff** (`Function`) + - πŸ“ Compute create/update/delete buckets by UUID+signature. + - Ζ’ **_build_target_signatures** (`Function`) + - πŸ“ Pull target metadata and normalize it into comparable signatures. + - Ζ’ **_build_risks** (`Function`) + - πŸ“ Build risk items for missing datasource, broken refs, overwrite, owner mismatch. +- πŸ“¦ **backend.src.core.migration.archive_parser** (`Module`) + - πŸ“ Parse Superset export ZIP archives into normalized object catalogs for diffing. + - πŸ—οΈ Layer: Core + - πŸ”’ Invariant: Parsing is read-only and never mutates archive files. + - πŸ”— DEPENDS_ON -> `backend.src.core.logger` + - β„‚ **MigrationArchiveParser** (`Class`) + - πŸ“ Extract normalized dashboards/charts/datasets metadata from ZIP archives. + - Ζ’ **extract_objects_from_zip** (`Function`) + - πŸ“ Extract object catalogs from Superset archive. + - Ζ’ **_collect_yaml_objects** (`Function`) + - πŸ“ Read and normalize YAML manifests for one object type. + - Ζ’ **_normalize_object_payload** (`Function`) + - πŸ“ Convert raw YAML payload to stable diff signature shape. +- πŸ“¦ **backend.src.core.migration.risk_assessor** (`Module`) + - πŸ“ Risk evaluation helpers for migration pre-flight reporting. + - πŸ—οΈ Layer: Core + - Ζ’ **index_by_uuid** (`Function`) + - πŸ“ Build UUID-index from normalized objects. + - Ζ’ **extract_owner_identifiers** (`Function`) + - πŸ“ Normalize owner payloads for stable comparison. + - Ζ’ **build_risks** (`Function`) + - πŸ“ Build risk list from computed diffs and target catalog state. + - Ζ’ **score_risks** (`Function`) + - πŸ“ Aggregate risk list into score and level. +- πŸ“¦ **backend.src.core.migration.__init__** (`Module`) `[TRIVIAL]` + - πŸ“ Namespace package for migration pre-flight orchestration components. + - πŸ—οΈ Layer: Core - πŸ“¦ **TaskLoggerModule** (`Module`) `[CRITICAL]` - πŸ“ Provides a dedicated logger for tasks with automatic source attribution. - πŸ—οΈ Layer: Core @@ -2591,6 +2818,12 @@ - πŸ“ Provides FastAPI endpoints for Git integration operations. - πŸ—οΈ Layer: API - πŸ”’ Invariant: All Git operations must be routed through GitService. + - Ζ’ **_build_no_repo_status_payload** (`Function`) + - πŸ“ Build a consistent status payload for dashboards without initialized repositories. + - Ζ’ **_handle_unexpected_git_route_error** (`Function`) + - πŸ“ Convert unexpected route-level exceptions to stable 500 API responses. + - Ζ’ **_resolve_repository_status** (`Function`) + - πŸ“ Resolve repository status for one dashboard with graceful NO_REPO semantics. - Ζ’ **get_git_configs** (`Function`) - πŸ“ List all configured Git servers. - Ζ’ **create_git_config** (`Function`) @@ -2623,6 +2856,8 @@ - πŸ“ View commit history for a dashboard's repository. - Ζ’ **get_repository_status** (`Function`) - πŸ“ Get current Git status for a dashboard repository. + - Ζ’ **get_repository_status_batch** (`Function`) + - πŸ“ Get Git statuses for multiple dashboard repositories in one request. - Ζ’ **get_repository_diff** (`Function`) - πŸ“ Get Git diff for a dashboard repository. - Ζ’ **generate_commit_message** (`Function`) @@ -2667,6 +2902,8 @@ - πŸ“ Fetch all dashboards from the specified environment for the grid. - Ζ’ **execute_migration** (`Function`) - πŸ“ Execute the migration of selected dashboards. + - Ζ’ **dry_run_migration** (`Function`) + - πŸ“ Build pre-flight diff and risk summary without applying migration. - Ζ’ **get_migration_settings** (`Function`) - πŸ“ Get current migration Cron string explicitly. - Ζ’ **update_migration_settings** (`Function`) @@ -2795,6 +3032,10 @@ - πŸ“ Schema for dashboard deployment requests. - β„‚ **RepoInitRequest** (`Class`) - πŸ“ Schema for repository initialization requests. + - β„‚ **RepoStatusBatchRequest** (`Class`) + - πŸ“ Schema for requesting repository statuses for multiple dashboards in a single call. + - β„‚ **RepoStatusBatchResponse** (`Class`) + - πŸ“ Schema for returning repository statuses keyed by dashboard ID. - πŸ“¦ **backend.src.api.routes.assistant** (`Module`) - πŸ“ API routes for LLM assistant command parsing and safe execution orchestration. - πŸ—οΈ Layer: API @@ -2865,26 +3106,30 @@ - πŸ“ Normalize intent entity value types from LLM output to route-compatible values. - Ζ’ **_confirmation_summary** (`Function`) - πŸ“ Build human-readable confirmation prompt for an intent before execution. - - Ζ’ **_clarification_text_for_intent** (`Function`) - - πŸ“ Convert technical missing-parameter errors into user-facing clarification prompts. - - Ζ’ **_plan_intent_with_llm** (`Function`) - - πŸ“ Use active LLM provider to select best tool/operation from dynamic catalog. - - Ζ’ **_authorize_intent** (`Function`) - - πŸ“ Validate user permissions for parsed intent before confirmation/dispatch. - - Ζ’ **_dispatch_intent** (`Function`) - - πŸ“ Execute parsed assistant intent via existing task/plugin/git services. - - Ζ’ **send_message** (`Function`) - - πŸ“ Parse assistant command, enforce safety gates, and dispatch executable intent. - - Ζ’ **confirm_operation** (`Function`) - - πŸ“ Execute previously requested risky operation after explicit user confirmation. - - Ζ’ **cancel_operation** (`Function`) - - πŸ“ Cancel pending risky operation and mark confirmation token as cancelled. - - Ζ’ **list_conversations** (`Function`) - - πŸ“ Return paginated conversation list for current user with archived flag and last message preview. - - Ζ’ **get_history** (`Function`) - - πŸ“ Retrieve paginated assistant conversation history for current user. - - Ζ’ **get_assistant_audit** (`Function`) - - πŸ“ Return assistant audit decisions for current user from persistent and in-memory stores. + - Ζ’ **_clarification_text_for_intent** (`Function`) + - πŸ“ Convert technical missing-parameter errors into user-facing clarification prompts. + - Ζ’ **_plan_intent_with_llm** (`Function`) + - πŸ“ Use active LLM provider to select best tool/operation from dynamic catalog. + - Ζ’ **_authorize_intent** (`Function`) + - πŸ“ Validate user permissions for parsed intent before confirmation/dispatch. + - Ζ’ **_dispatch_intent** (`Function`) + - πŸ“ Execute parsed assistant intent via existing task/plugin/git services. + - Ζ’ **send_message** (`Function`) + - πŸ“ Parse assistant command, enforce safety gates, and dispatch executable intent. + - Ζ’ **confirm_operation** (`Function`) + - πŸ“ Execute previously requested risky operation after explicit user confirmation. + - Ζ’ **cancel_operation** (`Function`) + - πŸ“ Cancel pending risky operation and mark confirmation token as cancelled. + - Ζ’ **list_conversations** (`Function`) + - πŸ“ Return paginated conversation list for current user with archived flag and last message preview. + - Ζ’ **delete_conversation** (`Function`) + - πŸ“ Soft-delete or hard-delete a conversation and clear its in-memory trace. + - Ζ’ **get_history** (`Function`) + - πŸ“ Retrieve paginated assistant conversation history for current user. + - Ζ’ **get_assistant_audit** (`Function`) + - πŸ“ Return assistant audit decisions for current user from persistent and in-memory stores. + - Ζ’ **_async_confirmation_summary** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - Ζ’ **_label** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) - πŸ“¦ **storage_routes** (`Module`) @@ -3020,6 +3265,52 @@ - πŸ“ Auto-detected function (orphan) - Ζ’ **_network_request** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) +- πŸ“¦ **backend.src.api.routes.__tests__.test_git_status_route** (`Module`) + - πŸ“ Validate status endpoint behavior for missing and error repository states. + - πŸ—οΈ Layer: Domain (Tests) + - πŸ”— CALLS -> `src.api.routes.git.get_repository_status` + - Ζ’ **test_get_repository_status_returns_no_repo_payload_for_missing_repo** (`Function`) + - πŸ“ Ensure missing local repository is represented as NO_REPO payload instead of an API error. + - Ζ’ **test_get_repository_status_propagates_non_404_http_exception** (`Function`) + - πŸ“ Ensure HTTP exceptions other than 404 are not masked. + - Ζ’ **test_get_repository_diff_propagates_http_exception** (`Function`) + - πŸ“ Ensure diff endpoint preserves domain HTTP errors from GitService. + - Ζ’ **test_get_history_wraps_unexpected_error_as_500** (`Function`) + - πŸ“ Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors. + - Ζ’ **test_commit_changes_wraps_unexpected_error_as_500** (`Function`) + - πŸ“ Ensure commit endpoint does not leak unexpected errors as 400. + - Ζ’ **test_get_repository_status_batch_returns_mixed_statuses** (`Function`) + - πŸ“ Ensure batch endpoint returns per-dashboard statuses in one response. + - Ζ’ **test_get_repository_status_batch_marks_item_as_error_on_service_failure** (`Function`) + - πŸ“ Ensure batch endpoint marks failed items as ERROR without failing entire request. + - Ζ’ **test_get_repository_status_batch_deduplicates_and_truncates_ids** (`Function`) + - πŸ“ Ensure batch endpoint protects server from oversized payloads. + - Ζ’ **_get_repo_path** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **get_status** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **_get_repo_path** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **get_status** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **get_diff** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **get_commit_history** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **commit_changes** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **_get_repo_path** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **get_status** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **_get_repo_path** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **get_status** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **_get_repo_path** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **get_status** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - πŸ“¦ **backend.tests.test_reports_openapi_conformance** (`Module`) - πŸ“ Validate implemented reports payload shape against OpenAPI-required top-level contract fields. - πŸ—οΈ Layer: Domain (Tests) @@ -3221,6 +3512,8 @@ - πŸ“ Auto-detected function (orphan) - Ζ’ **get_environments** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) + - Ζ’ **get_config** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - Ζ’ **__init__** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) - Ζ’ **filter** (`Function`) `[TRIVIAL]` @@ -3249,6 +3542,8 @@ - πŸ“ Auto-detected function (orphan) - Ζ’ **rollback** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) + - Ζ’ **run** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - πŸ“¦ **backend.src.api.routes.__tests__.test_migration_routes** (`Module`) - πŸ“ Unit tests for migration API route handlers. - πŸ—οΈ Layer: API @@ -3294,6 +3589,10 @@ - πŸ“ Auto-detected function (orphan) - Ζ’ **test_execute_migration_invalid_env_raises_400** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) + - Ζ’ **test_dry_run_migration_returns_diff_and_risk** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **test_dry_run_migration_rejects_same_environment** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - πŸ“¦ **backend.src.models.config** (`Module`) - πŸ“ Defines database schema for persisted application configuration. - πŸ—οΈ Layer: Domain @@ -3508,7 +3807,15 @@ - πŸ“ Fetch dashboards from environment with Git status and last task status - πŸ”— CALLS -> `SupersetClient.get_dashboards_summary` - πŸ”— CALLS -> `self._get_git_status_for_dashboard` - - πŸ”— CALLS -> `self._get_last_task_for_resource` + - πŸ”— CALLS -> `self._get_last_llm_task_for_dashboard` + - Ζ’ **get_dashboards_page_with_status** (`Function`) + - πŸ“ Fetch one dashboard page from environment and enrich only that page with status metadata. + - Ζ’ **_get_last_llm_task_for_dashboard** (`Function`) + - πŸ“ Get most recent LLM validation task for a dashboard in an environment + - Ζ’ **_normalize_task_status** (`Function`) + - πŸ“ Normalize task status to stable uppercase values for UI/API projections + - Ζ’ **_normalize_validation_status** (`Function`) + - πŸ“ Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN - Ζ’ **get_datasets_with_status** (`Function`) - πŸ“ Fetch datasets from environment with mapping progress and last task status - πŸ”— CALLS -> `SupersetClient.get_datasets_summary` @@ -3524,6 +3831,8 @@ - πŸ“ Extract resource name from task params - Ζ’ **_extract_resource_type_from_task** (`Function`) - πŸ“ Extract resource type from task params + - Ζ’ **_task_time** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) - πŸ“¦ **backend.src.services.llm_prompt_templates** (`Module`) - πŸ“ Provide default LLM prompt templates and normalization helpers for runtime usage. - πŸ—οΈ Layer: Domain @@ -4521,3 +4830,17 @@ - πŸ“ Auto-detected function (orphan) - Ζ’ **test_transform_yaml_nonexistent_file** (`Function`) `[TRIVIAL]` - πŸ“ Auto-detected function (orphan) +- πŸ“¦ **backend.tests.core.migration.test_dry_run_orchestrator** (`Module`) + - πŸ“ Unit tests for MigrationDryRunService diff and risk computation contracts. + - πŸ—οΈ Layer: Domain + - Ζ’ **_load_fixture** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **_make_session** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) + - Ζ’ **test_migration_dry_run_service_builds_diff_and_risk** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) +- πŸ“¦ **backend.tests.core.migration.test_archive_parser** (`Module`) + - πŸ“ Unit tests for MigrationArchiveParser ZIP extraction contract. + - πŸ—οΈ Layer: Domain + - Ζ’ **test_extract_objects_from_zip_collects_all_types** (`Function`) `[TRIVIAL]` + - πŸ“ Auto-detected function (orphan) diff --git a/backend/src/api/routes/__tests__/test_assistant_api.py b/backend/src/api/routes/__tests__/test_assistant_api.py index 202f238..8eb252f 100644 --- a/backend/src/api/routes/__tests__/test_assistant_api.py +++ b/backend/src/api/routes/__tests__/test_assistant_api.py @@ -76,11 +76,15 @@ class _FakeTaskManager: class _FakeConfigManager: def get_environments(self): return [ - SimpleNamespace(id="dev", name="Development"), - SimpleNamespace(id="prod", name="Production"), + SimpleNamespace(id="dev", name="Development", url="http://dev", credentials_id="dev", username="fakeuser", password="fakepassword"), + SimpleNamespace(id="prod", name="Production", url="http://prod", credentials_id="prod", username="fakeuser", password="fakepassword"), ] - + def get_config(self): + return SimpleNamespace( + settings=SimpleNamespace(migration_sync_cron="0 0 * * *"), + environments=self.get_environments() + ) # [/DEF:_FakeConfigManager:Class] # [DEF:_admin_user:Function] # @TIER: TRIVIAL @@ -645,5 +649,49 @@ def test_confirm_nonexistent_id_returns_404(): assert exc.value.status_code == 404 -# [/DEF:test_guarded_operation_confirm_roundtrip:Function] +# [DEF:test_migration_with_dry_run_includes_summary:Function] +# @PURPOSE: Migration command with dry run flag must return the dry run summary in confirmation text. +# @PRE: user specifies a migration with --dry-run flag. +# @POST: Response state is needs_confirmation and text contains dry-run summary counts. +def test_migration_with_dry_run_includes_summary(monkeypatch): + import src.core.migration.dry_run_orchestrator as dry_run_module + from unittest.mock import MagicMock + _clear_assistant_state() + task_manager = _FakeTaskManager() + db = _FakeDb() + + class _FakeDryRunService: + def run(self, selection, source_client, target_client, db_session): + return { + "summary": { + "dashboards": {"create": 1, "update": 0, "delete": 0}, + "charts": {"create": 3, "update": 2, "delete": 1}, + "datasets": {"create": 0, "update": 1, "delete": 0} + } + } + + monkeypatch.setattr(dry_run_module, "MigrationDryRunService", _FakeDryRunService) + + import src.core.superset_client as superset_client_module + monkeypatch.setattr(superset_client_module, "SupersetClient", lambda env: MagicMock()) + + start = _run_async( + assistant_module.send_message( + request=assistant_module.AssistantMessageRequest( + message="миграция с dev Π½Π° prod для Π΄Π°ΡˆΠ±ΠΎΡ€Π΄Π° 10 --dry-run" + ), + current_user=_admin_user(), + task_manager=task_manager, + config_manager=_FakeConfigManager(), + db=db, + ) + ) + + assert start.state == "needs_confirmation" + assert "ΠΎΡ‚Ρ‡Π΅Ρ‚ dry-run: Π’ΠšΠ›" in start.text + assert "ΠžΡ‚Ρ‡Π΅Ρ‚ dry-run:" in start.text + assert "создано Π½ΠΎΠ²Ρ‹Ρ… ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠ²: 4" in start.text + assert "ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΎ: 3" in start.text + assert "ΡƒΠ΄Π°Π»Π΅Π½ΠΎ: 1" in start.text +# [/DEF:test_migration_with_dry_run_includes_summary:Function] # [/DEF:backend.src.api.routes.__tests__.test_assistant_api:Module] diff --git a/backend/src/api/routes/__tests__/test_git_status_route.py b/backend/src/api/routes/__tests__/test_git_status_route.py new file mode 100644 index 0000000..30bb416 --- /dev/null +++ b/backend/src/api/routes/__tests__/test_git_status_route.py @@ -0,0 +1,198 @@ +# [DEF:backend.src.api.routes.__tests__.test_git_status_route:Module] +# @TIER: STANDARD +# @SEMANTICS: tests, git, api, status, no_repo +# @PURPOSE: Validate status endpoint behavior for missing and error repository states. +# @LAYER: Domain (Tests) +# @RELATION: CALLS -> src.api.routes.git.get_repository_status + +from fastapi import HTTPException +import pytest +import asyncio + +from src.api.routes import git as git_routes + + +# [DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function] +# @PURPOSE: Ensure missing local repository is represented as NO_REPO payload instead of an API error. +# @PRE: GitService.get_status raises HTTPException(404). +# @POST: Route returns a deterministic NO_REPO status payload. +def test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypatch): + class MissingRepoGitService: + def _get_repo_path(self, dashboard_id: int) -> str: + return f"/tmp/missing-repo-{dashboard_id}" + + def get_status(self, dashboard_id: int) -> dict: + raise AssertionError("get_status must not be called when repository path is missing") + + monkeypatch.setattr(git_routes, "git_service", MissingRepoGitService()) + + response = asyncio.run(git_routes.get_repository_status(34)) + + assert response["sync_status"] == "NO_REPO" + assert response["sync_state"] == "NO_REPO" + assert response["has_repo"] is False + assert response["current_branch"] is None +# [/DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function] + + +# [DEF:test_get_repository_status_propagates_non_404_http_exception:Function] +# @PURPOSE: Ensure HTTP exceptions other than 404 are not masked. +# @PRE: GitService.get_status raises HTTPException with non-404 status. +# @POST: Raised exception preserves original status and detail. +def test_get_repository_status_propagates_non_404_http_exception(monkeypatch): + class ConflictGitService: + def _get_repo_path(self, dashboard_id: int) -> str: + return f"/tmp/existing-repo-{dashboard_id}" + + def get_status(self, dashboard_id: int) -> dict: + raise HTTPException(status_code=409, detail="Conflict") + + monkeypatch.setattr(git_routes, "git_service", ConflictGitService()) + monkeypatch.setattr(git_routes.os.path, "exists", lambda _path: True) + + with pytest.raises(HTTPException) as exc_info: + asyncio.run(git_routes.get_repository_status(34)) + + assert exc_info.value.status_code == 409 + assert exc_info.value.detail == "Conflict" +# [/DEF:test_get_repository_status_propagates_non_404_http_exception:Function] + + +# [DEF:test_get_repository_diff_propagates_http_exception:Function] +# @PURPOSE: Ensure diff endpoint preserves domain HTTP errors from GitService. +# @PRE: GitService.get_diff raises HTTPException. +# @POST: Endpoint raises same HTTPException values. +def test_get_repository_diff_propagates_http_exception(monkeypatch): + class DiffGitService: + def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str: + raise HTTPException(status_code=404, detail="Repository missing") + + monkeypatch.setattr(git_routes, "git_service", DiffGitService()) + + with pytest.raises(HTTPException) as exc_info: + asyncio.run(git_routes.get_repository_diff(12)) + + assert exc_info.value.status_code == 404 + assert exc_info.value.detail == "Repository missing" +# [/DEF:test_get_repository_diff_propagates_http_exception:Function] + + +# [DEF:test_get_history_wraps_unexpected_error_as_500:Function] +# @PURPOSE: Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors. +# @PRE: GitService.get_commit_history raises ValueError. +# @POST: Endpoint returns HTTPException with status 500 and route context. +def test_get_history_wraps_unexpected_error_as_500(monkeypatch): + class HistoryGitService: + def get_commit_history(self, dashboard_id: int, limit: int = 50): + raise ValueError("broken parser") + + monkeypatch.setattr(git_routes, "git_service", HistoryGitService()) + + with pytest.raises(HTTPException) as exc_info: + asyncio.run(git_routes.get_history(12)) + + assert exc_info.value.status_code == 500 + assert exc_info.value.detail == "get_history failed: broken parser" +# [/DEF:test_get_history_wraps_unexpected_error_as_500:Function] + + +# [DEF:test_commit_changes_wraps_unexpected_error_as_500:Function] +# @PURPOSE: Ensure commit endpoint does not leak unexpected errors as 400. +# @PRE: GitService.commit_changes raises RuntimeError. +# @POST: Endpoint raises HTTPException(500) with route context. +def test_commit_changes_wraps_unexpected_error_as_500(monkeypatch): + class CommitGitService: + def commit_changes(self, dashboard_id: int, message: str, files): + raise RuntimeError("index lock") + + class CommitPayload: + message = "test" + files = ["dashboards/a.yaml"] + + monkeypatch.setattr(git_routes, "git_service", CommitGitService()) + + with pytest.raises(HTTPException) as exc_info: + asyncio.run(git_routes.commit_changes(12, CommitPayload())) + + assert exc_info.value.status_code == 500 + assert exc_info.value.detail == "commit_changes failed: index lock" +# [/DEF:test_commit_changes_wraps_unexpected_error_as_500:Function] + + +# [DEF:test_get_repository_status_batch_returns_mixed_statuses:Function] +# @PURPOSE: Ensure batch endpoint returns per-dashboard statuses in one response. +# @PRE: Some repositories are missing and some are initialized. +# @POST: Returned map includes resolved status for each requested dashboard ID. +def test_get_repository_status_batch_returns_mixed_statuses(monkeypatch): + class BatchGitService: + def _get_repo_path(self, dashboard_id: int) -> str: + return f"/tmp/repo-{dashboard_id}" + + def get_status(self, dashboard_id: int) -> dict: + if dashboard_id == 2: + return {"sync_state": "SYNCED", "sync_status": "OK"} + raise HTTPException(status_code=404, detail="not found") + + monkeypatch.setattr(git_routes, "git_service", BatchGitService()) + monkeypatch.setattr(git_routes.os.path, "exists", lambda path: path.endswith("/repo-2")) + + class BatchRequest: + dashboard_ids = [1, 2] + + response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest())) + + assert response.statuses["1"]["sync_status"] == "NO_REPO" + assert response.statuses["2"]["sync_state"] == "SYNCED" +# [/DEF:test_get_repository_status_batch_returns_mixed_statuses:Function] + + +# [DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function] +# @PURPOSE: Ensure batch endpoint marks failed items as ERROR without failing entire request. +# @PRE: GitService raises non-HTTP exception for one dashboard. +# @POST: Failed dashboard status is marked as ERROR. +def test_get_repository_status_batch_marks_item_as_error_on_service_failure(monkeypatch): + class BatchErrorGitService: + def _get_repo_path(self, dashboard_id: int) -> str: + return f"/tmp/repo-{dashboard_id}" + + def get_status(self, dashboard_id: int) -> dict: + raise RuntimeError("boom") + + monkeypatch.setattr(git_routes, "git_service", BatchErrorGitService()) + monkeypatch.setattr(git_routes.os.path, "exists", lambda _path: True) + + class BatchRequest: + dashboard_ids = [9] + + response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest())) + + assert response.statuses["9"]["sync_status"] == "ERROR" + assert response.statuses["9"]["sync_state"] == "ERROR" +# [/DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function] + + +# [DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function] +# @PURPOSE: Ensure batch endpoint protects server from oversized payloads. +# @PRE: request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries. +# @POST: Result contains unique IDs up to configured cap. +def test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch): + class SafeBatchGitService: + def _get_repo_path(self, dashboard_id: int) -> str: + return f"/tmp/repo-{dashboard_id}" + + def get_status(self, dashboard_id: int) -> dict: + return {"sync_state": "SYNCED", "sync_status": "OK"} + + monkeypatch.setattr(git_routes, "git_service", SafeBatchGitService()) + monkeypatch.setattr(git_routes.os.path, "exists", lambda _path: True) + + class BatchRequest: + dashboard_ids = [1, 1] + list(range(2, 90)) + + response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest())) + + assert len(response.statuses) == git_routes.MAX_REPOSITORY_STATUS_BATCH + assert "1" in response.statuses +# [/DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function] + +# [/DEF:backend.src.api.routes.__tests__.test_git_status_route:Module] diff --git a/backend/src/api/routes/assistant.py b/backend/src/api/routes/assistant.py index 3c548fd..55521a5 100644 --- a/backend/src/api/routes/assistant.py +++ b/backend/src/api/routes/assistant.py @@ -810,6 +810,9 @@ def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any if any(k in lower for k in ["ΠΌΠΈΠ³Ρ€Π°Ρ†", "migration", "migrate"]): src = _extract_id(lower, [r"(?:с|from)\s+([a-z0-9_-]+)"]) tgt = _extract_id(lower, [r"(?:Π½Π°|to)\s+([a-z0-9_-]+)"]) + dry_run = "--dry-run" in lower or "dry run" in lower + replace_db_config = "--replace-db-config" in lower + fix_cross_filters = "--fix-cross-filters" not in lower # Default true usually, but let's say test uses --dry-run is_dangerous = _is_production_env(tgt, config_manager) return { "domain": "migration", @@ -818,10 +821,13 @@ def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any "dashboard_id": int(dashboard_id) if dashboard_id else None, "source_env": src, "target_env": tgt, + "dry_run": dry_run, + "replace_db_config": replace_db_config, + "fix_cross_filters": True, }, "confidence": 0.95 if dashboard_id and src and tgt else 0.72, "risk_level": "dangerous" if is_dangerous else "guarded", - "requires_confirmation": is_dangerous, + "requires_confirmation": is_dangerous or dry_run, } # Backup @@ -1057,7 +1063,7 @@ _SAFE_OPS = {"show_capabilities", "get_task_status"} # @PURPOSE: Build human-readable confirmation prompt for an intent before execution. # @PRE: intent contains operation and entities fields. # @POST: Returns descriptive Russian-language text ending with confirmation prompt. -def _confirmation_summary(intent: Dict[str, Any]) -> str: +async def _async_confirmation_summary(intent: Dict[str, Any], config_manager: ConfigManager, db: Session) -> str: operation = intent.get("operation", "") entities = intent.get("entities", {}) descriptions: Dict[str, str] = { @@ -1085,8 +1091,67 @@ def _confirmation_summary(intent: Dict[str, Any]) -> str: tgt=_label(entities.get("target_env")), dataset=_label(entities.get("dataset_id")), ) + + if operation == "execute_migration": + flags = [] + flags.append("ΠΌΠ°ΠΏΠΏΠΈΠ½Π³ Π‘Π”: " + ("Π’ΠšΠ›" if _coerce_query_bool(entities.get("replace_db_config", False)) else "Π’Π«ΠšΠ›")) + flags.append("исправлСниС ΠΊΡ€ΠΎΡΡΡ„ΠΈΠ»ΡŒΡ‚Ρ€ΠΎΠ²: " + ("Π’ΠšΠ›" if _coerce_query_bool(entities.get("fix_cross_filters", True)) else "Π’Π«ΠšΠ›")) + dry_run_enabled = _coerce_query_bool(entities.get("dry_run", False)) + flags.append("ΠΎΡ‚Ρ‡Π΅Ρ‚ dry-run: " + ("Π’ΠšΠ›" if dry_run_enabled else "Π’Π«ΠšΠ›")) + text += f" ({', '.join(flags)})" + + if dry_run_enabled: + try: + from ...core.migration.dry_run_orchestrator import MigrationDryRunService + from ...models.dashboard import DashboardSelection + from ...core.superset_client import SupersetClient + + src_token = entities.get("source_env") + tgt_token = entities.get("target_env") + dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token) + + if dashboard_id and src_token and tgt_token: + src_env_id = _resolve_env_id(src_token, config_manager) + tgt_env_id = _resolve_env_id(tgt_token, config_manager) + + if src_env_id and tgt_env_id: + env_map = {env.id: env for env in config_manager.get_environments()} + source_env = env_map.get(src_env_id) + target_env = env_map.get(tgt_env_id) + + if source_env and target_env and source_env.id != target_env.id: + selection = DashboardSelection( + source_env_id=source_env.id, + target_env_id=target_env.id, + selected_ids=[dashboard_id], + replace_db_config=_coerce_query_bool(entities.get("replace_db_config", False)), + fix_cross_filters=_coerce_query_bool(entities.get("fix_cross_filters", True)) + ) + service = MigrationDryRunService() + source_client = SupersetClient(source_env) + target_client = SupersetClient(target_env) + report = service.run(selection, source_client, target_client, db) + + s = report.get("summary", {}) + dash_s = s.get("dashboards", {}) + charts_s = s.get("charts", {}) + ds_s = s.get("datasets", {}) + + # Determine main actions counts + creates = dash_s.get("create", 0) + charts_s.get("create", 0) + ds_s.get("create", 0) + updates = dash_s.get("update", 0) + charts_s.get("update", 0) + ds_s.get("update", 0) + deletes = dash_s.get("delete", 0) + charts_s.get("delete", 0) + ds_s.get("delete", 0) + + text += f"\n\nΠžΡ‚Ρ‡Π΅Ρ‚ dry-run:\n- Π‘ΡƒΠ΄Π΅Ρ‚ создано Π½ΠΎΠ²Ρ‹Ρ… ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠ²: {creates}\n- Π‘ΡƒΠ΄Π΅Ρ‚ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΎ: {updates}\n- Π‘ΡƒΠ΄Π΅Ρ‚ ΡƒΠ΄Π°Π»Π΅Π½ΠΎ: {deletes}" + else: + text += "\n\n(НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΎΡ‚Ρ‡Π΅Ρ‚ dry-run: Π½Π΅Π²Π΅Ρ€Π½Ρ‹Π΅ окруТСния)." + except Exception as e: + import traceback + logger.warning("[assistant.dry_run_summary][failed] Exception: %s\n%s", e, traceback.format_exc()) + text += f"\n\n(НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΎΡ‚Ρ‡Π΅Ρ‚ dry-run: {e})." + return f"Π’Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚ΡŒ: {text}. ΠŸΠΎΠ΄Ρ‚Π²Π΅Ρ€Π΄ΠΈΡ‚Π΅ ΠΈΠ»ΠΈ ΠΎΡ‚ΠΌΠ΅Π½ΠΈΡ‚Π΅." -# [/DEF:_confirmation_summary:Function] +# [/DEF:_async_confirmation_summary:Function] # [DEF:_clarification_text_for_intent:Function] @@ -1176,7 +1241,8 @@ async def _plan_intent_with_llm( ] ) except Exception as exc: - logger.warning(f"[assistant.planner][fallback] LLM planner unavailable: {exc}") + import traceback + logger.warning(f"[assistant.planner][fallback] LLM planner unavailable: {exc}\n{traceback.format_exc()}") return None if not isinstance(response, dict): return None @@ -1580,7 +1646,7 @@ async def send_message( ) CONFIRMATIONS[confirmation_id] = confirm _persist_confirmation(db, confirm) - text = _confirmation_summary(intent) + text = await _async_confirmation_summary(intent, config_manager, db) _append_history( user_id, conversation_id, @@ -1895,6 +1961,39 @@ async def list_conversations( # [/DEF:list_conversations:Function] +# [DEF:delete_conversation:Function] +# @PURPOSE: Soft-delete or hard-delete a conversation and clear its in-memory trace. +# @PRE: conversation_id belongs to current_user. +# @POST: Conversation records are removed from DB and CONVERSATIONS cache. +@router.delete("/conversations/{conversation_id}") +async def delete_conversation( + conversation_id: str, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + with belief_scope("assistant.conversations.delete"): + user_id = current_user.id + + # 1. Remove from in-memory cache + key = (user_id, conversation_id) + if key in CONVERSATIONS: + del CONVERSATIONS[key] + + # 2. Delete from database + deleted_count = db.query(AssistantMessageRecord).filter( + AssistantMessageRecord.user_id == user_id, + AssistantMessageRecord.conversation_id == conversation_id + ).delete() + + db.commit() + + if deleted_count == 0: + raise HTTPException(status_code=404, detail="Conversation not found or already deleted") + + return {"status": "success", "deleted": deleted_count, "conversation_id": conversation_id} +# [/DEF:delete_conversation:Function] + + @router.get("/history") # [DEF:get_history:Function] # @PURPOSE: Retrieve paginated assistant conversation history for current user. diff --git a/backend/src/api/routes/dashboards.py b/backend/src/api/routes/dashboards.py index 05042ac..598ac56 100644 --- a/backend/src/api/routes/dashboards.py +++ b/backend/src/api/routes/dashboards.py @@ -42,6 +42,7 @@ from ...dependencies import get_config_manager, get_task_manager, get_resource_s from ...core.logger import logger, belief_scope from ...core.superset_client import SupersetClient from ...core.utils.network import DashboardNotFoundError +from ...services.resource_service import ResourceService # [/SECTION] router = APIRouter(prefix="/api/dashboards", tags=["Dashboards"]) @@ -197,31 +198,66 @@ async def get_dashboards( try: # Get all tasks for status lookup all_tasks = task_manager.get_all_tasks() - - # Fetch dashboards with status using ResourceService - dashboards = await resource_service.get_dashboards_with_status( - env, - all_tasks, - include_git_status=False, - ) - - # Apply search filter if provided - if search: - search_lower = search.lower() - dashboards = [ - d for d in dashboards - if search_lower in d.get('title', '').lower() - or search_lower in d.get('slug', '').lower() - ] - - # Calculate pagination - total = len(dashboards) - total_pages = (total + page_size - 1) // page_size if total > 0 else 1 - start_idx = (page - 1) * page_size - end_idx = start_idx + page_size - - # Slice dashboards for current page - paginated_dashboards = dashboards[start_idx:end_idx] + + # Fast path: real ResourceService -> one Superset page call per API request. + if isinstance(resource_service, ResourceService): + try: + page_payload = await resource_service.get_dashboards_page_with_status( + env, + all_tasks, + page=page, + page_size=page_size, + search=search, + include_git_status=False, + ) + paginated_dashboards = page_payload["dashboards"] + total = page_payload["total"] + total_pages = page_payload["total_pages"] + except Exception as page_error: + logger.warning( + "[get_dashboards][Action] Page-based fetch failed; using compatibility fallback: %s", + page_error, + ) + dashboards = await resource_service.get_dashboards_with_status( + env, + all_tasks, + include_git_status=False, + ) + + if search: + search_lower = search.lower() + dashboards = [ + d for d in dashboards + if search_lower in d.get('title', '').lower() + or search_lower in d.get('slug', '').lower() + ] + + total = len(dashboards) + total_pages = (total + page_size - 1) // page_size if total > 0 else 1 + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + paginated_dashboards = dashboards[start_idx:end_idx] + else: + # Compatibility path for mocked services in route tests. + dashboards = await resource_service.get_dashboards_with_status( + env, + all_tasks, + include_git_status=False, + ) + + if search: + search_lower = search.lower() + dashboards = [ + d for d in dashboards + if search_lower in d.get('title', '').lower() + or search_lower in d.get('slug', '').lower() + ] + + total = len(dashboards) + total_pages = (total + page_size - 1) // page_size if total > 0 else 1 + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + paginated_dashboards = dashboards[start_idx:end_idx] logger.info(f"[get_dashboards][Coherence:OK] Returning {len(paginated_dashboards)} dashboards (page {page}/{total_pages}, total: {total})") diff --git a/backend/src/api/routes/git.py b/backend/src/api/routes/git.py index a52e4a7..f8a537d 100644 --- a/backend/src/api/routes/git.py +++ b/backend/src/api/routes/git.py @@ -14,6 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List, Optional import typing +import os from src.dependencies import get_config_manager, has_permission from src.core.database import get_db from src.models.git import GitServerConfig, GitRepository @@ -21,7 +22,8 @@ from src.api.routes.git_schemas import ( GitServerConfigSchema, GitServerConfigCreate, BranchSchema, BranchCreate, BranchCheckout, CommitSchema, CommitCreate, - DeploymentEnvironmentSchema, DeployRequest, RepoInitRequest + DeploymentEnvironmentSchema, DeployRequest, RepoInitRequest, + RepoStatusBatchRequest, RepoStatusBatchResponse, ) from src.services.git_service import GitService from src.core.logger import logger, belief_scope @@ -33,6 +35,69 @@ from ...services.llm_prompt_templates import ( router = APIRouter(tags=["git"]) git_service = GitService() +MAX_REPOSITORY_STATUS_BATCH = 50 + + +# [DEF:_build_no_repo_status_payload:Function] +# @PURPOSE: Build a consistent status payload for dashboards without initialized repositories. +# @PRE: None. +# @POST: Returns a stable payload compatible with frontend repository status parsing. +# @RETURN: dict +def _build_no_repo_status_payload() -> dict: + return { + "is_dirty": False, + "untracked_files": [], + "modified_files": [], + "staged_files": [], + "current_branch": None, + "upstream_branch": None, + "has_upstream": False, + "ahead_count": 0, + "behind_count": 0, + "is_diverged": False, + "sync_state": "NO_REPO", + "sync_status": "NO_REPO", + "has_repo": False, + } +# [/DEF:_build_no_repo_status_payload:Function] + + +# [DEF:_handle_unexpected_git_route_error:Function] +# @PURPOSE: Convert unexpected route-level exceptions to stable 500 API responses. +# @PRE: `error` is a non-HTTPException instance. +# @POST: Raises HTTPException(500) with route-specific context. +# @PARAM: route_name (str) +# @PARAM: error (Exception) +def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None: + logger.error(f"[{route_name}][Coherence:Failed] {error}") + raise HTTPException(status_code=500, detail=f"{route_name} failed: {str(error)}") +# [/DEF:_handle_unexpected_git_route_error:Function] + + +# [DEF:_resolve_repository_status:Function] +# @PURPOSE: Resolve repository status for one dashboard with graceful NO_REPO semantics. +# @PRE: `dashboard_id` is a valid integer. +# @POST: Returns standard status payload or `NO_REPO` payload when repository path is absent. +# @PARAM: dashboard_id (int) +# @RETURN: dict +def _resolve_repository_status(dashboard_id: int) -> dict: + repo_path = git_service._get_repo_path(dashboard_id) + if not os.path.exists(repo_path): + logger.debug( + f"[get_repository_status][Action] Repository is not initialized for dashboard {dashboard_id}" + ) + return _build_no_repo_status_payload() + + try: + return git_service.get_status(dashboard_id) + except HTTPException as e: + if e.status_code == 404: + logger.debug( + f"[get_repository_status][Action] Repository is not initialized for dashboard {dashboard_id}" + ) + return _build_no_repo_status_payload() + raise +# [/DEF:_resolve_repository_status:Function] # [DEF:get_git_configs:Function] # @PURPOSE: List all configured Git servers. @@ -153,7 +218,9 @@ async def init_repository( except Exception as e: db.rollback() logger.error(f"[init_repository][Coherence:Failed] Failed to init repository: {e}") - raise HTTPException(status_code=400, detail=str(e)) + if isinstance(e, HTTPException): + raise + _handle_unexpected_git_route_error("init_repository", e) # [/DEF:init_repository:Function] # [DEF:get_branches:Function] @@ -170,8 +237,10 @@ async def get_branches( with belief_scope("get_branches"): try: return git_service.list_branches(dashboard_id) + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=404, detail=str(e)) + _handle_unexpected_git_route_error("get_branches", e) # [/DEF:get_branches:Function] # [DEF:create_branch:Function] @@ -190,8 +259,10 @@ async def create_branch( try: git_service.create_branch(dashboard_id, branch_data.name, branch_data.from_branch) return {"status": "success"} + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + _handle_unexpected_git_route_error("create_branch", e) # [/DEF:create_branch:Function] # [DEF:checkout_branch:Function] @@ -210,8 +281,10 @@ async def checkout_branch( try: git_service.checkout_branch(dashboard_id, checkout_data.name) return {"status": "success"} + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + _handle_unexpected_git_route_error("checkout_branch", e) # [/DEF:checkout_branch:Function] # [DEF:commit_changes:Function] @@ -230,8 +303,10 @@ async def commit_changes( try: git_service.commit_changes(dashboard_id, commit_data.message, commit_data.files) return {"status": "success"} + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + _handle_unexpected_git_route_error("commit_changes", e) # [/DEF:commit_changes:Function] # [DEF:push_changes:Function] @@ -248,8 +323,10 @@ async def push_changes( try: git_service.push_changes(dashboard_id) return {"status": "success"} + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + _handle_unexpected_git_route_error("push_changes", e) # [/DEF:push_changes:Function] # [DEF:pull_changes:Function] @@ -266,8 +343,10 @@ async def pull_changes( try: git_service.pull_changes(dashboard_id) return {"status": "success"} + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + _handle_unexpected_git_route_error("pull_changes", e) # [/DEF:pull_changes:Function] # [DEF:sync_dashboard:Function] @@ -291,8 +370,10 @@ async def sync_dashboard( "dashboard_id": dashboard_id, "source_env_id": source_env_id }) + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + _handle_unexpected_git_route_error("sync_dashboard", e) # [/DEF:sync_dashboard:Function] # [DEF:get_environments:Function] @@ -338,8 +419,10 @@ async def deploy_dashboard( "dashboard_id": dashboard_id, "environment_id": deploy_data.environment_id }) + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + _handle_unexpected_git_route_error("deploy_dashboard", e) # [/DEF:deploy_dashboard:Function] # [DEF:get_history:Function] @@ -358,14 +441,16 @@ async def get_history( with belief_scope("get_history"): try: return git_service.get_commit_history(dashboard_id, limit) + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=404, detail=str(e)) + _handle_unexpected_git_route_error("get_history", e) # [/DEF:get_history:Function] # [DEF:get_repository_status:Function] # @PURPOSE: Get current Git status for a dashboard repository. -# @PRE: `dashboard_id` repository exists. -# @POST: Returns the status of the working directory (staged, unstaged, untracked). +# @PRE: `dashboard_id` is a valid integer. +# @POST: Returns repository status; if repo is not initialized, returns `NO_REPO` payload. # @PARAM: dashboard_id (int) # @RETURN: dict @router.get("/repositories/{dashboard_id}/status") @@ -375,11 +460,57 @@ async def get_repository_status( ): with belief_scope("get_repository_status"): try: - return git_service.get_status(dashboard_id) + return _resolve_repository_status(dashboard_id) + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + _handle_unexpected_git_route_error("get_repository_status", e) # [/DEF:get_repository_status:Function] + +# [DEF:get_repository_status_batch:Function] +# @PURPOSE: Get Git statuses for multiple dashboard repositories in one request. +# @PRE: `request.dashboard_ids` is provided. +# @POST: Returns `statuses` map where each key is dashboard ID and value is repository status payload. +# @PARAM: request (RepoStatusBatchRequest) +# @RETURN: RepoStatusBatchResponse +@router.post("/repositories/status/batch", response_model=RepoStatusBatchResponse) +async def get_repository_status_batch( + request: RepoStatusBatchRequest, + _ = Depends(has_permission("plugin:git", "EXECUTE")) +): + with belief_scope("get_repository_status_batch"): + dashboard_ids = list(dict.fromkeys(request.dashboard_ids)) + if len(dashboard_ids) > MAX_REPOSITORY_STATUS_BATCH: + logger.warning( + "[get_repository_status_batch][Action] Batch size %s exceeds limit %s. Truncating request.", + len(dashboard_ids), + MAX_REPOSITORY_STATUS_BATCH, + ) + dashboard_ids = dashboard_ids[:MAX_REPOSITORY_STATUS_BATCH] + + statuses = {} + for dashboard_id in dashboard_ids: + try: + statuses[str(dashboard_id)] = _resolve_repository_status(dashboard_id) + except HTTPException: + statuses[str(dashboard_id)] = { + **_build_no_repo_status_payload(), + "sync_state": "ERROR", + "sync_status": "ERROR", + } + except Exception as e: + logger.error( + f"[get_repository_status_batch][Coherence:Failed] Failed for dashboard {dashboard_id}: {e}" + ) + statuses[str(dashboard_id)] = { + **_build_no_repo_status_payload(), + "sync_state": "ERROR", + "sync_status": "ERROR", + } + return RepoStatusBatchResponse(statuses=statuses) +# [/DEF:get_repository_status_batch:Function] + # [DEF:get_repository_diff:Function] # @PURPOSE: Get Git diff for a dashboard repository. # @PRE: `dashboard_id` repository exists. @@ -399,8 +530,10 @@ async def get_repository_diff( try: diff_text = git_service.get_diff(dashboard_id, file_path, staged) return diff_text + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + _handle_unexpected_git_route_error("get_repository_diff", e) # [/DEF:get_repository_diff:Function] # [DEF:generate_commit_message:Function] @@ -466,9 +599,10 @@ async def generate_commit_message( ) return {"message": message} + except HTTPException: + raise except Exception as e: - logger.error(f"Failed to generate commit message: {e}") - raise HTTPException(status_code=400, detail=str(e)) + _handle_unexpected_git_route_error("generate_commit_message", e) # [/DEF:generate_commit_message:Function] # [/DEF:backend.src.api.routes.git:Module] diff --git a/backend/src/api/routes/git_schemas.py b/backend/src/api/routes/git_schemas.py index 56eb67d..759fc79 100644 --- a/backend/src/api/routes/git_schemas.py +++ b/backend/src/api/routes/git_schemas.py @@ -9,7 +9,7 @@ # @INVARIANT: All schemas must be compatible with the FastAPI router. from pydantic import BaseModel, Field -from typing import List, Optional +from typing import Any, Dict, List, Optional from datetime import datetime from src.models.git import GitProvider, GitStatus, SyncStatus @@ -141,4 +141,17 @@ class RepoInitRequest(BaseModel): remote_url: str # [/DEF:RepoInitRequest:Class] -# [/DEF:backend.src.api.routes.git_schemas:Module] \ No newline at end of file +# [DEF:RepoStatusBatchRequest:Class] +# @PURPOSE: Schema for requesting repository statuses for multiple dashboards in a single call. +class RepoStatusBatchRequest(BaseModel): + dashboard_ids: List[int] = Field(default_factory=list, description="Dashboard IDs to resolve repository statuses for") +# [/DEF:RepoStatusBatchRequest:Class] + + +# [DEF:RepoStatusBatchResponse:Class] +# @PURPOSE: Schema for returning repository statuses keyed by dashboard ID. +class RepoStatusBatchResponse(BaseModel): + statuses: Dict[str, Dict[str, Any]] +# [/DEF:RepoStatusBatchResponse:Class] + +# [/DEF:backend.src.api.routes.git_schemas:Module] diff --git a/backend/src/core/superset_client.py b/backend/src/core/superset_client.py index 1f32f84..f743c44 100644 --- a/backend/src/core/superset_client.py +++ b/backend/src/core/superset_client.py @@ -108,6 +108,41 @@ class SupersetClient: return total_count, paginated_data # [/DEF:get_dashboards:Function] + # [DEF:get_dashboards_page:Function] + # @PURPOSE: Fetches a single dashboards page from Superset without iterating all pages. + # @PARAM: query (Optional[Dict]) - Query with page/page_size and optional columns. + # @PRE: Client is authenticated. + # @POST: Returns total count and one page of dashboards. + # @RETURN: Tuple[int, List[Dict]] + def get_dashboards_page(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: + with belief_scope("get_dashboards_page"): + validated_query = self._validate_query_params(query or {}) + if "columns" not in validated_query: + validated_query["columns"] = [ + "slug", + "id", + "changed_on_utc", + "dashboard_title", + "published", + "created_by", + "changed_by", + "changed_by_name", + "owners", + ] + + response_json = cast( + Dict[str, Any], + self.network.request( + method="GET", + endpoint="/dashboard/", + params={"q": json.dumps(validated_query)}, + ), + ) + result = response_json.get("result", []) + total_count = response_json.get("count", len(result)) + return total_count, result + # [/DEF:get_dashboards_page:Function] + # [DEF:get_dashboards_summary:Function] # @PURPOSE: Fetches dashboard metadata optimized for the grid. # @PRE: Client is authenticated. @@ -148,6 +183,65 @@ class SupersetClient: return result # [/DEF:get_dashboards_summary:Function] + # [DEF:get_dashboards_summary_page:Function] + # @PURPOSE: Fetches one page of dashboard metadata optimized for the grid. + # @PARAM: page (int) - 1-based page number from API route contract. + # @PARAM: page_size (int) - Number of items per page. + # @PRE: page >= 1 and page_size > 0. + # @POST: Returns mapped summaries and total dashboard count. + # @RETURN: Tuple[int, List[Dict]] + def get_dashboards_summary_page( + self, + page: int, + page_size: int, + search: Optional[str] = None, + ) -> Tuple[int, List[Dict]]: + with belief_scope("SupersetClient.get_dashboards_summary_page"): + query: Dict[str, Any] = { + "page": max(page - 1, 0), + "page_size": page_size, + } + normalized_search = (search or "").strip() + if normalized_search: + # Superset list API supports filter objects with `opr` operator. + # `ct` -> contains (ILIKE on most Superset backends). + query["filters"] = [ + { + "col": "dashboard_title", + "opr": "ct", + "value": normalized_search, + } + ] + + total_count, dashboards = self.get_dashboards_page(query=query) + + result = [] + for dash in dashboards: + owners = self._extract_owner_labels(dash.get("owners")) + if not owners: + owners = self._extract_owner_labels( + [dash.get("created_by"), dash.get("changed_by")], + ) + + result.append({ + "id": dash.get("id"), + "title": dash.get("dashboard_title"), + "last_modified": dash.get("changed_on_utc"), + "status": "published" if dash.get("published") else "draft", + "created_by": self._extract_user_display( + None, + dash.get("created_by"), + ), + "modified_by": self._extract_user_display( + dash.get("changed_by_name"), + dash.get("changed_by"), + ), + "owners": owners, + }) + + return total_count, result + # [/DEF:get_dashboards_summary_page:Function] + # [DEF:_extract_owner_labels:Function] # @PURPOSE: Normalize dashboard owners payload to stable display labels. # @PRE: owners payload can be scalar, object or list. diff --git a/backend/src/services/git_service.py b/backend/src/services/git_service.py index 4c4e691..3b6a3cf 100644 --- a/backend/src/services/git_service.py +++ b/backend/src/services/git_service.py @@ -302,12 +302,62 @@ class GitService: except (ValueError, Exception): has_commits = False + current_branch = repo.active_branch.name + tracking_branch = None + has_upstream = False + ahead_count = 0 + behind_count = 0 + + try: + tracking_branch = repo.active_branch.tracking_branch() + has_upstream = tracking_branch is not None + except Exception: + tracking_branch = None + has_upstream = False + + if has_upstream and tracking_branch is not None: + try: + # Commits present locally but not in upstream. + ahead_count = sum( + 1 for _ in repo.iter_commits(f"{tracking_branch.name}..{current_branch}") + ) + # Commits present in upstream but not local. + behind_count = sum( + 1 for _ in repo.iter_commits(f"{current_branch}..{tracking_branch.name}") + ) + except Exception: + ahead_count = 0 + behind_count = 0 + + is_dirty = repo.is_dirty(untracked_files=True) + untracked_files = repo.untracked_files + modified_files = [item.a_path for item in repo.index.diff(None)] + staged_files = [item.a_path for item in repo.index.diff("HEAD")] if has_commits else [] + is_diverged = ahead_count > 0 and behind_count > 0 + + if is_diverged: + sync_state = "DIVERGED" + elif behind_count > 0: + sync_state = "BEHIND_REMOTE" + elif ahead_count > 0: + sync_state = "AHEAD_REMOTE" + elif is_dirty or modified_files or staged_files or untracked_files: + sync_state = "CHANGES" + else: + sync_state = "SYNCED" + return { - "is_dirty": repo.is_dirty(untracked_files=True), - "untracked_files": repo.untracked_files, - "modified_files": [item.a_path for item in repo.index.diff(None)], - "staged_files": [item.a_path for item in repo.index.diff("HEAD")] if has_commits else [], - "current_branch": repo.active_branch.name + "is_dirty": is_dirty, + "untracked_files": untracked_files, + "modified_files": modified_files, + "staged_files": staged_files, + "current_branch": current_branch, + "upstream_branch": tracking_branch.name if tracking_branch is not None else None, + "has_upstream": has_upstream, + "ahead_count": ahead_count, + "behind_count": behind_count, + "is_diverged": is_diverged, + "sync_state": sync_state, } # [/DEF:get_status:Function] @@ -411,4 +461,4 @@ class GitService: # [/DEF:test_connection:Function] # [/DEF:GitService:Class] -# [/DEF:backend.src.services.git_service:Module] \ No newline at end of file +# [/DEF:backend.src.services.git_service:Module] diff --git a/backend/src/services/resource_service.py b/backend/src/services/resource_service.py index 383a568..2606838 100644 --- a/backend/src/services/resource_service.py +++ b/backend/src/services/resource_service.py @@ -79,6 +79,67 @@ class ResourceService: return result # [/DEF:get_dashboards_with_status:Function] + # [DEF:get_dashboards_page_with_status:Function] + # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata. + # @PRE: env is valid; page >= 1; page_size > 0. + # @POST: Returns page items plus total counters without scanning all pages locally. + # @PARAM: env (Environment) - Source environment. + # @PARAM: tasks (Optional[List[Task]]) - Tasks for latest LLM status. + # @PARAM: page (int) - 1-based page number. + # @PARAM: page_size (int) - Page size. + # @RETURN: Dict[str, Any] - {"dashboards": List[Dict], "total": int, "total_pages": int} + async def get_dashboards_page_with_status( + self, + env: Any, + tasks: Optional[List[Task]] = None, + page: int = 1, + page_size: int = 10, + search: Optional[str] = None, + include_git_status: bool = True, + ) -> Dict[str, Any]: + with belief_scope( + "get_dashboards_page_with_status", + f"env={env.id}, page={page}, page_size={page_size}, search={search}", + ): + client = SupersetClient(env) + total, dashboards_page = client.get_dashboards_summary_page( + page=page, + page_size=page_size, + search=search, + ) + + result = [] + for dashboard in dashboards_page: + dashboard_dict = dashboard + dashboard_id = dashboard_dict.get("id") + + if include_git_status: + dashboard_dict["git_status"] = self._get_git_status_for_dashboard(dashboard_id) + else: + dashboard_dict["git_status"] = None + + dashboard_dict["last_task"] = self._get_last_llm_task_for_dashboard( + dashboard_id, + env.id, + tasks, + ) + result.append(dashboard_dict) + + total_pages = (total + page_size - 1) // page_size if total > 0 else 1 + logger.info( + "[ResourceService][Coherence:OK] Fetched dashboards page %s/%s (%s items, total=%s)", + page, + total_pages, + len(result), + total, + ) + return { + "dashboards": result, + "total": total, + "total_pages": total_pages, + } + # [/DEF:get_dashboards_page_with_status:Function] + # [DEF:_get_last_llm_task_for_dashboard:Function] # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment # @PRE: dashboard_id is a valid integer identifier diff --git a/frontend/src/components/RepositoryDashboardGrid.svelte b/frontend/src/components/RepositoryDashboardGrid.svelte new file mode 100644 index 0000000..8d70612 --- /dev/null +++ b/frontend/src/components/RepositoryDashboardGrid.svelte @@ -0,0 +1,613 @@ + + + + + + +
+ +
+ +
+ + {#if selectedIds.length > 0} +
+ + + + + + + {$t.git?.selected_count.replace( + "{count}", + String(selectedIds.length), + )} + +
+ {/if} + + +
+ + + + + + + + + + + {#each paginatedDashboards as dashboard (dashboard.id)} + + + + + + + {/each} + +
+ + handleSelectAll((e.target as HTMLInputElement).checked)} + class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" + /> + handleSort("title")} + > + {$t.dashboard.title} + {sortColumn === "title" + ? sortDirection === "asc" + ? "↑" + : "↓" + : ""} + handleSort("last_modified")} + > + {$t.dashboard.last_modified} + {sortColumn === "last_modified" + ? sortDirection === "asc" + ? "↑" + : "↓" + : ""} + handleSort("status")} + > + {$t.dashboard.status} + {sortColumn === "status" + ? sortDirection === "asc" + ? "↑" + : "↓" + : ""} +
+ + handleSelectionChange( + dashboard.id, + (e.target as HTMLInputElement).checked, + )} + class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" + /> + {dashboard.title}{new Date(dashboard.last_modified).toLocaleDateString()} + + {getStatusLabel(dashboard)} + +
+
+ + +
+
+ {($t.dashboard?.showing ) + .replace("{start}", (currentPage * pageSize + 1).toString()) + .replace( + "{end}", + Math.min( + (currentPage + 1) * pageSize, + sortedDashboards.length, + ).toString(), + ) + .replace("{total}", sortedDashboards.length.toString())} +
+
+ + +
+
+
+ +{#if showGitManager && gitDashboardId} + +{/if} + + + + diff --git a/frontend/src/components/git/GitManager.svelte b/frontend/src/components/git/GitManager.svelte index 211c2a3..8188701 100644 --- a/frontend/src/components/git/GitManager.svelte +++ b/frontend/src/components/git/GitManager.svelte @@ -157,17 +157,56 @@ } // [/DEF:handlePull:Function] + // [DEF:closeModal:Function] + /** + * @purpose Π—Π°ΠΊΡ€Ρ‹Π²Π°Π΅Ρ‚ модальноС ΠΎΠΊΠ½ΠΎ управлСния Git. + * @post show=false. + */ + function closeModal() { + show = false; + } + // [/DEF:closeModal:Function] + + // [DEF:handleBackdropClick:Function] + /** + * @purpose Π—Π°ΠΊΡ€Ρ‹Π²Π°Π΅Ρ‚ ΠΌΠΎΠ΄Π°Π»ΠΊΡƒ ΠΏΠΎ ΠΊΠ»ΠΈΠΊΡƒ Π½Π° ΠΏΠΎΠ΄Π»ΠΎΠΆΠΊΡƒ. + * @pre Π‘ΠΎΠ±Ρ‹Ρ‚ΠΈΠ΅ ΠΏΡ€ΠΈΡˆΠ»ΠΎ с овСрлСя. + * @post show=false. + */ + function handleBackdropClick(event) { + if (event.target === event.currentTarget) { + closeModal(); + } + } + // [/DEF:handleBackdropClick:Function] + onMount(checkStatus); {#if show} -
-
+
+
event.stopPropagation()} + > +
{$t.common?.id}: {dashboardId}
-
{#each conversations as convo (convo.conversation_id)} - +
+ + +
{/each} {#if loadingConversations}