Compare commits

..

14 Commits

133 changed files with 89259 additions and 2581 deletions

4
.gitignore vendored
View File

@@ -66,6 +66,4 @@ backend/mappings.db
backend/tasks.db
# Git Integration repositories
backend/git_repos/
backend/logs

View File

@@ -25,6 +25,13 @@ Auto-generated from all feature plans. Last updated: 2025-12-19
- Filesystem (local git repo), SQLite (for GitServerConfig, Environment) (011-git-integration-dashboard)
- Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI, SvelteKit, GitPython (or CLI git), Pydantic, SQLAlchemy, Superset API (011-git-integration-dashboard)
- SQLite (for config/history), Filesystem (local Git repositories) (011-git-integration-dashboard)
- Node.js 18+ (Frontend Build), Svelte 5.x + SvelteKit, Tailwind CSS, `date-fns` (existing) (013-unify-frontend-css)
- LocalStorage (for language preference) (013-unify-frontend-css)
- Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit (Frontend) (014-file-storage-ui)
- Local Filesystem (for artifacts), Config (for storage path) (014-file-storage-ui)
- Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit + Tailwind CSS (Frontend) (015-frontend-nav-redesign)
- N/A (UI reorganization and API integration) (015-frontend-nav-redesign)
- SQLite (`auth.db`) for Users, Roles, Permissions, and Mappings. (016-multi-user-auth)
- Python 3.9+ (Backend), Node.js 18+ (Frontend Build) (001-plugin-arch-svelte-ui)
@@ -45,9 +52,9 @@ cd src; pytest; ruff check .
Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions
## Recent Changes
- 011-git-integration-dashboard: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI, SvelteKit, GitPython (or CLI git), Pydantic, SQLAlchemy, Superset API
- 011-git-integration-dashboard: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI, SvelteKit, GitPython (or CLI git), Pydantic, SQLAlchemy, Superset API
- 011-git-integration-dashboard: Added Python 3.9+, Node.js 18+
- 016-multi-user-auth: Added Python 3.9+ (Backend), Node.js 18+ (Frontend)
- 015-frontend-nav-redesign: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit + Tailwind CSS (Frontend)
- 014-file-storage-ui: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit (Frontend)
<!-- MANUAL ADDITIONS START -->

View File

@@ -1,11 +1,10 @@
<!--
SYNC IMPACT REPORT
Version: 1.7.1 (Simplified Workflow)
Version: 1.8.0 (Frontend Unification)
Changes:
- Simplified Generation Workflow to a single phase: Code Generation from `tasks.md`.
- Removed multi-phase Architecture/Implementation split to streamline development.
- Added Principle VIII: Unified Frontend Experience (Mandating Design System & i18n).
Templates Status:
- .specify/templates/plan-template.md: ✅ Aligned (Dynamic check).
- .specify/templates/plan-template.md: ✅ Aligned.
- .specify/templates/spec-template.md: ✅ Aligned.
- .specify/templates/tasks-template.md: ✅ Aligned.
-->
@@ -37,6 +36,11 @@ To maintain semantic coherence, code must adhere to the complexity limits (Modul
### VII. Everything is a Plugin
All functional extensions, tools, or major features must be implemented as modular Plugins inheriting from `PluginBase`. Logic should not reside in standalone services or scripts unless strictly necessary for core infrastructure. This ensures a unified execution model via the `TaskManager`, consistent logging, and modularity.
### VIII. Unified Frontend Experience
To ensure a consistent and accessible user experience, all frontend implementations must strictly adhere to the unified design and localization standards.
- **Component Reusability**: All UI elements MUST utilize the standardized Svelte component library (`src/lib/ui`) and centralized design tokens. Ad-hoc styling and hardcoded values are prohibited.
- **Internationalization (i18n)**: All user-facing text MUST be extracted to the translation system (`src/lib/i18n`). Hardcoded strings in the UI are prohibited.
## File Structure Standards
Refer to **Section III (File Structure Standard)** in `semantic_protocol.md` for the authoritative definitions of:
- Python Module Headers (`.py`)
@@ -64,4 +68,4 @@ This Constitution establishes the "Semantic Code Generation Protocol" as the sup
- **Amendments**: Changes to core principles require a Constitution amendment. Changes to technical syntax require a Protocol update.
- **Compliance**: Failure to adhere to the Protocol constitutes a build failure.
**Version**: 1.7.1 | **Ratified**: 2025-12-19 | **Last Amended**: 2026-01-13
**Version**: 1.8.0 | **Ratified**: 2025-12-19 | **Last Amended**: 2026-01-26

Submodule backend/backend/git_repos/12 updated: d592fa7ed5...f46772443a

View File

@@ -1,269 +0,0 @@
2025-12-20 19:55:11,325 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 19:55:11,325 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 19:55:11,327 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 43, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 21:01:49,905 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 21:01:49,906 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 21:01:49,988 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 21:01:49,990 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 66, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 22:42:32,538 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 22:42:32,538 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 22:42:32,583 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 22:42:32,587 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 66, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/backend/.venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 22:54:29,770 - INFO - [BackupPlugin][Entry] Starting backup for .
2025-12-20 22:54:29,771 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 22:54:29,831 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 22:54:29,833 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 66, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/backend/.venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 22:54:34,078 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 22:54:34,078 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 22:54:34,079 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 22:54:34,079 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 66, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/backend/.venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 22:59:25,060 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 22:59:25,060 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 22:59:25,114 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 22:59:25,117 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 66, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/backend/.venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 23:00:31,156 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 23:00:31,156 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 23:00:31,157 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 23:00:31,162 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 66, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/backend/.venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 23:00:34,710 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 23:00:34,710 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 23:00:34,710 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 23:00:34,711 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 66, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/backend/.venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 23:01:43,894 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 23:01:43,894 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 23:01:43,895 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 23:01:43,895 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 66, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/backend/.venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 23:04:07,731 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 23:04:07,731 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 23:04:07,732 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 23:04:07,732 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 66, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/backend/.venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 23:06:39,641 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 23:06:39,642 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 23:06:39,687 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 23:06:39,689 - CRITICAL - [setup_clients][Failure] Critical error during client initialization: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
Traceback (most recent call last):
File "/home/user/ss-tools/superset_tool/utils/init_clients.py", line 66, in setup_clients
config = SupersetConfig(
^^^^^^^^^^^^^^^
File "/home/user/ss-tools/backend/.venv/lib/python3.12/site-packages/pydantic/main.py", line 250, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for SupersetConfig
base_url
Value error, Invalid URL format: https://superset.bebesh.ru. Must include '/api/v1'. [type=value_error, input_value='https://superset.bebesh.ru', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/value_error
2025-12-20 23:30:36,090 - INFO - [BackupPlugin][Entry] Starting backup for superset.
2025-12-20 23:30:36,093 - INFO - [setup_clients][Enter] Starting Superset clients initialization.
2025-12-20 23:30:36,128 - INFO - [setup_clients][Action] Loading environments from ConfigManager
2025-12-20 23:30:36,129 - INFO - [SupersetClient.__init__][Enter] Initializing SupersetClient.
2025-12-20 23:30:36,129 - INFO - [APIClient.__init__][Entry] Initializing APIClient.
2025-12-20 23:30:36,130 - WARNING - [_init_session][State] SSL verification disabled.
2025-12-20 23:30:36,130 - INFO - [APIClient.__init__][Exit] APIClient initialized.
2025-12-20 23:30:36,130 - INFO - [SupersetClient.__init__][Exit] SupersetClient initialized.
2025-12-20 23:30:36,130 - INFO - [get_dashboards][Enter] Fetching dashboards.
2025-12-20 23:30:36,131 - INFO - [authenticate][Enter] Authenticating to https://superset.bebesh.ru/api/v1
2025-12-20 23:30:36,897 - INFO - [authenticate][Exit] Authenticated successfully.
2025-12-20 23:30:37,527 - INFO - [get_dashboards][Exit] Found 11 dashboards.
2025-12-20 23:30:37,527 - INFO - [BackupPlugin][Progress] Found 11 dashboards to export in superset.
2025-12-20 23:30:37,529 - INFO - [export_dashboard][Enter] Exporting dashboard 11.
2025-12-20 23:30:38,224 - INFO - [export_dashboard][Exit] Exported dashboard 11 to dashboard_export_20251220T203037.zip.
2025-12-20 23:30:38,225 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:38,226 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/FCC New Coder Survey 2018/dashboard_export_20251220T203037.zip
2025-12-20 23:30:38,227 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/FCC New Coder Survey 2018
2025-12-20 23:30:38,230 - INFO - [export_dashboard][Enter] Exporting dashboard 10.
2025-12-20 23:30:38,438 - INFO - [export_dashboard][Exit] Exported dashboard 10 to dashboard_export_20251220T203038.zip.
2025-12-20 23:30:38,438 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:38,439 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/COVID Vaccine Dashboard/dashboard_export_20251220T203038.zip
2025-12-20 23:30:38,439 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/COVID Vaccine Dashboard
2025-12-20 23:30:38,440 - INFO - [export_dashboard][Enter] Exporting dashboard 9.
2025-12-20 23:30:38,853 - INFO - [export_dashboard][Exit] Exported dashboard 9 to dashboard_export_20251220T203038.zip.
2025-12-20 23:30:38,853 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:38,856 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/Sales Dashboard/dashboard_export_20251220T203038.zip
2025-12-20 23:30:38,856 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/Sales Dashboard
2025-12-20 23:30:38,858 - INFO - [export_dashboard][Enter] Exporting dashboard 8.
2025-12-20 23:30:38,939 - INFO - [export_dashboard][Exit] Exported dashboard 8 to dashboard_export_20251220T203038.zip.
2025-12-20 23:30:38,940 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:38,941 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/Unicode Test/dashboard_export_20251220T203038.zip
2025-12-20 23:30:38,941 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/Unicode Test
2025-12-20 23:30:38,942 - INFO - [export_dashboard][Enter] Exporting dashboard 7.
2025-12-20 23:30:39,148 - INFO - [export_dashboard][Exit] Exported dashboard 7 to dashboard_export_20251220T203038.zip.
2025-12-20 23:30:39,148 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:39,149 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/Video Game Sales/dashboard_export_20251220T203038.zip
2025-12-20 23:30:39,149 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/Video Game Sales
2025-12-20 23:30:39,150 - INFO - [export_dashboard][Enter] Exporting dashboard 6.
2025-12-20 23:30:39,689 - INFO - [export_dashboard][Exit] Exported dashboard 6 to dashboard_export_20251220T203039.zip.
2025-12-20 23:30:39,689 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:39,690 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/Featured Charts/dashboard_export_20251220T203039.zip
2025-12-20 23:30:39,691 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/Featured Charts
2025-12-20 23:30:39,692 - INFO - [export_dashboard][Enter] Exporting dashboard 5.
2025-12-20 23:30:39,960 - INFO - [export_dashboard][Exit] Exported dashboard 5 to dashboard_export_20251220T203039.zip.
2025-12-20 23:30:39,960 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:39,961 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/Slack Dashboard/dashboard_export_20251220T203039.zip
2025-12-20 23:30:39,961 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/Slack Dashboard
2025-12-20 23:30:39,962 - INFO - [export_dashboard][Enter] Exporting dashboard 4.
2025-12-20 23:30:40,196 - INFO - [export_dashboard][Exit] Exported dashboard 4 to dashboard_export_20251220T203039.zip.
2025-12-20 23:30:40,196 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:40,197 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/deck.gl Demo/dashboard_export_20251220T203039.zip
2025-12-20 23:30:40,197 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/deck.gl Demo
2025-12-20 23:30:40,198 - INFO - [export_dashboard][Enter] Exporting dashboard 3.
2025-12-20 23:30:40,745 - INFO - [export_dashboard][Exit] Exported dashboard 3 to dashboard_export_20251220T203040.zip.
2025-12-20 23:30:40,746 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:40,760 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/Misc Charts/dashboard_export_20251220T203040.zip
2025-12-20 23:30:40,761 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/Misc Charts
2025-12-20 23:30:40,762 - INFO - [export_dashboard][Enter] Exporting dashboard 2.
2025-12-20 23:30:40,928 - INFO - [export_dashboard][Exit] Exported dashboard 2 to dashboard_export_20251220T203040.zip.
2025-12-20 23:30:40,929 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:40,930 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/USA Births Names/dashboard_export_20251220T203040.zip
2025-12-20 23:30:40,931 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/USA Births Names
2025-12-20 23:30:40,932 - INFO - [export_dashboard][Enter] Exporting dashboard 1.
2025-12-20 23:30:41,582 - INFO - [export_dashboard][Exit] Exported dashboard 1 to dashboard_export_20251220T203040.zip.
2025-12-20 23:30:41,582 - INFO - [save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: False
2025-12-20 23:30:41,749 - INFO - [save_and_unpack_dashboard][State] Dashboard saved to: backups/SUPERSET/World Bank's Data/dashboard_export_20251220T203040.zip
2025-12-20 23:30:41,750 - INFO - [archive_exports][Enter] Managing archive in backups/SUPERSET/World Bank's Data
2025-12-20 23:30:41,752 - INFO - [consolidate_archive_folders][Enter] Consolidating archives in backups/SUPERSET
2025-12-20 23:30:41,753 - INFO - [remove_empty_directories][Enter] Starting cleanup of empty directories in backups/SUPERSET
2025-12-20 23:30:41,758 - INFO - [remove_empty_directories][Exit] Removed 0 empty directories.
2025-12-20 23:30:41,758 - INFO - [BackupPlugin][CoherenceCheck:Passed] Backup logic completed for superset.

View File

@@ -1,10 +1,17 @@
#!/usr/bin/env python3
"""Script to delete tasks with RUNNING status from the database."""
# [DEF:backend.delete_running_tasks:Module]
# @PURPOSE: Script to delete tasks with RUNNING status from the database.
# @LAYER: Utility
# @SEMANTICS: maintenance, database, cleanup
from sqlalchemy.orm import Session
from src.core.database import TasksSessionLocal
from src.models.task import TaskRecord
# [DEF:delete_running_tasks:Function]
# @PURPOSE: Delete all tasks with RUNNING status from the database.
# @PRE: Database is accessible and TaskRecord model is defined.
# @POST: All tasks with status 'RUNNING' are removed from the database.
def delete_running_tasks():
"""Delete all tasks with RUNNING status from the database."""
session: Session = TasksSessionLocal()
@@ -30,6 +37,8 @@ def delete_running_tasks():
print(f"Error deleting tasks: {e}")
finally:
session.close()
# [/DEF:delete_running_tasks:Function]
if __name__ == "__main__":
delete_running_tasks()
# [/DEF:backend.delete_running_tasks:Module]

79101
backend/logs/app.log.1 Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1 +1 @@
from . import plugins, tasks, settings, connections, environments, mappings, migration, git
from . import plugins, tasks, settings, connections, environments, mappings, migration, git, storage

View File

@@ -23,7 +23,7 @@ router = APIRouter()
# [DEF:ScheduleSchema:DataClass]
class ScheduleSchema(BaseModel):
enabled: bool = False
cron_expression: str = Field(..., pattern=r'^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|((((\d+,)*\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})$')
cron_expression: str = Field(..., pattern=r'^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|((((\d+,)*\d+|(\d+(\/|-)\d+)|\d+|\*) ?){4,6})$')
# [/DEF:ScheduleSchema:DataClass]
# [DEF:EnvironmentResponse:DataClass]

View File

@@ -30,6 +30,8 @@ git_service = GitService()
# [DEF:get_git_configs:Function]
# @PURPOSE: List all configured Git servers.
# @PRE: Database session `db` is available.
# @POST: Returns a list of all GitServerConfig objects from the database.
# @RETURN: List[GitServerConfigSchema]
@router.get("/config", response_model=List[GitServerConfigSchema])
async def get_git_configs(db: Session = Depends(get_db)):
@@ -39,6 +41,8 @@ async def get_git_configs(db: Session = Depends(get_db)):
# [DEF:create_git_config:Function]
# @PURPOSE: Register a new Git server configuration.
# @PRE: `config` contains valid GitServerConfigCreate data.
# @POST: A new GitServerConfig record is created in the database.
# @PARAM: config (GitServerConfigCreate)
# @RETURN: GitServerConfigSchema
@router.post("/config", response_model=GitServerConfigSchema)
@@ -53,6 +57,8 @@ async def create_git_config(config: GitServerConfigCreate, db: Session = Depends
# [DEF:delete_git_config:Function]
# @PURPOSE: Remove a Git server configuration.
# @PRE: `config_id` corresponds to an existing configuration.
# @POST: The configuration record is removed from the database.
# @PARAM: config_id (str)
@router.delete("/config/{config_id}")
async def delete_git_config(config_id: str, db: Session = Depends(get_db)):
@@ -68,6 +74,8 @@ async def delete_git_config(config_id: str, db: Session = Depends(get_db)):
# [DEF:test_git_config:Function]
# @PURPOSE: Validate connection to a Git server using provided credentials.
# @PRE: `config` contains provider, url, and pat.
# @POST: Returns success if the connection is validated via GitService.
# @PARAM: config (GitServerConfigCreate)
@router.post("/config/test")
async def test_git_config(config: GitServerConfigCreate):
@@ -81,6 +89,8 @@ async def test_git_config(config: GitServerConfigCreate):
# [DEF:init_repository:Function]
# @PURPOSE: Link a dashboard to a Git repository and perform initial clone/init.
# @PRE: `dashboard_id` exists and `init_data` contains valid config_id and remote_url.
# @POST: Repository is initialized on disk and a GitRepository record is saved in DB.
# @PARAM: dashboard_id (int)
# @PARAM: init_data (RepoInitRequest)
@router.post("/repositories/{dashboard_id}/init")
@@ -123,6 +133,8 @@ async def init_repository(dashboard_id: int, init_data: RepoInitRequest, db: Ses
# [DEF:get_branches:Function]
# @PURPOSE: List all branches for a dashboard's repository.
# @PRE: Repository for `dashboard_id` is initialized.
# @POST: Returns a list of branches from the local repository.
# @PARAM: dashboard_id (int)
# @RETURN: List[BranchSchema]
@router.get("/repositories/{dashboard_id}/branches", response_model=List[BranchSchema])
@@ -136,6 +148,8 @@ async def get_branches(dashboard_id: int):
# [DEF:create_branch:Function]
# @PURPOSE: Create a new branch in the dashboard's repository.
# @PRE: `dashboard_id` repository exists and `branch_data` has name and from_branch.
# @POST: A new branch is created in the local repository.
# @PARAM: dashboard_id (int)
# @PARAM: branch_data (BranchCreate)
@router.post("/repositories/{dashboard_id}/branches")
@@ -150,6 +164,8 @@ async def create_branch(dashboard_id: int, branch_data: BranchCreate):
# [DEF:checkout_branch:Function]
# @PURPOSE: Switch the dashboard's repository to a specific branch.
# @PRE: `dashboard_id` repository exists and branch `checkout_data.name` exists.
# @POST: The local repository HEAD is moved to the specified branch.
# @PARAM: dashboard_id (int)
# @PARAM: checkout_data (BranchCheckout)
@router.post("/repositories/{dashboard_id}/checkout")
@@ -164,6 +180,8 @@ async def checkout_branch(dashboard_id: int, checkout_data: BranchCheckout):
# [DEF:commit_changes:Function]
# @PURPOSE: Stage and commit changes in the dashboard's repository.
# @PRE: `dashboard_id` repository exists and `commit_data` has message and files.
# @POST: Specified files are staged and a new commit is created.
# @PARAM: dashboard_id (int)
# @PARAM: commit_data (CommitCreate)
@router.post("/repositories/{dashboard_id}/commit")
@@ -178,6 +196,8 @@ async def commit_changes(dashboard_id: int, commit_data: CommitCreate):
# [DEF:push_changes:Function]
# @PURPOSE: Push local commits to the remote repository.
# @PRE: `dashboard_id` repository exists and has a remote configured.
# @POST: Local commits are pushed to the remote repository.
# @PARAM: dashboard_id (int)
@router.post("/repositories/{dashboard_id}/push")
async def push_changes(dashboard_id: int):
@@ -191,6 +211,8 @@ async def push_changes(dashboard_id: int):
# [DEF:pull_changes:Function]
# @PURPOSE: Pull changes from the remote repository.
# @PRE: `dashboard_id` repository exists and has a remote configured.
# @POST: Remote changes are fetched and merged into the local branch.
# @PARAM: dashboard_id (int)
@router.post("/repositories/{dashboard_id}/pull")
async def pull_changes(dashboard_id: int):
@@ -204,6 +226,8 @@ async def pull_changes(dashboard_id: int):
# [DEF:sync_dashboard:Function]
# @PURPOSE: Sync dashboard state from Superset to Git using the GitPlugin.
# @PRE: `dashboard_id` is valid; GitPlugin is available.
# @POST: Dashboard YAMLs are exported from Superset and committed to Git.
# @PARAM: dashboard_id (int)
# @PARAM: source_env_id (Optional[str])
@router.post("/repositories/{dashboard_id}/sync")
@@ -223,6 +247,8 @@ async def sync_dashboard(dashboard_id: int, source_env_id: typing.Optional[str]
# [DEF:get_environments:Function]
# @PURPOSE: List all deployment environments.
# @PRE: Config manager is accessible.
# @POST: Returns a list of DeploymentEnvironmentSchema objects.
# @RETURN: List[DeploymentEnvironmentSchema]
@router.get("/environments", response_model=List[DeploymentEnvironmentSchema])
async def get_environments(config_manager=Depends(get_config_manager)):
@@ -240,6 +266,8 @@ async def get_environments(config_manager=Depends(get_config_manager)):
# [DEF:deploy_dashboard:Function]
# @PURPOSE: Deploy dashboard from Git to a target environment.
# @PRE: `dashboard_id` and `deploy_data.environment_id` are valid.
# @POST: Dashboard YAMLs are read from Git and imported into the target Superset.
# @PARAM: dashboard_id (int)
# @PARAM: deploy_data (DeployRequest)
@router.post("/repositories/{dashboard_id}/deploy")
@@ -259,6 +287,8 @@ async def deploy_dashboard(dashboard_id: int, deploy_data: DeployRequest):
# [DEF:get_history:Function]
# @PURPOSE: View commit history for a dashboard's repository.
# @PRE: `dashboard_id` repository exists.
# @POST: Returns a list of recent commits from the repository.
# @PARAM: dashboard_id (int)
# @PARAM: limit (int)
# @RETURN: List[CommitSchema]
@@ -273,6 +303,8 @@ async def get_history(dashboard_id: int, limit: int = 50):
# [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).
# @PARAM: dashboard_id (int)
# @RETURN: dict
@router.get("/repositories/{dashboard_id}/status")
@@ -286,6 +318,8 @@ async def get_repository_status(dashboard_id: int):
# [DEF:get_repository_diff:Function]
# @PURPOSE: Get Git diff for a dashboard repository.
# @PRE: `dashboard_id` repository exists.
# @POST: Returns the diff text for the specified file or all changes.
# @PARAM: dashboard_id (int)
# @PARAM: file_path (Optional[str])
# @PARAM: staged (bool)

View File

@@ -14,6 +14,7 @@ from uuid import UUID
from src.models.git import GitProvider, GitStatus, SyncStatus
# [DEF:GitServerConfigBase:Class]
# @PURPOSE: Base schema for Git server configuration attributes.
class GitServerConfigBase(BaseModel):
name: str = Field(..., description="Display name for the Git server")
provider: GitProvider = Field(..., description="Git provider (GITHUB, GITLAB, GITEA)")
@@ -23,12 +24,14 @@ class GitServerConfigBase(BaseModel):
# [/DEF:GitServerConfigBase:Class]
# [DEF:GitServerConfigCreate:Class]
# @PURPOSE: Schema for creating a new Git server configuration.
class GitServerConfigCreate(GitServerConfigBase):
"""Schema for creating a new Git server configuration."""
pass
# [/DEF:GitServerConfigCreate:Class]
# [DEF:GitServerConfigSchema:Class]
# @PURPOSE: Schema for representing a Git server configuration with metadata.
class GitServerConfigSchema(GitServerConfigBase):
"""Schema for representing a Git server configuration with metadata."""
id: str
@@ -40,6 +43,7 @@ class GitServerConfigSchema(GitServerConfigBase):
# [/DEF:GitServerConfigSchema:Class]
# [DEF:GitRepositorySchema:Class]
# @PURPOSE: Schema for tracking a local Git repository linked to a dashboard.
class GitRepositorySchema(BaseModel):
"""Schema for tracking a local Git repository linked to a dashboard."""
id: str
@@ -55,6 +59,7 @@ class GitRepositorySchema(BaseModel):
# [/DEF:GitRepositorySchema:Class]
# [DEF:BranchSchema:Class]
# @PURPOSE: Schema for representing a Git branch metadata.
class BranchSchema(BaseModel):
"""Schema for representing a Git branch."""
name: str
@@ -64,6 +69,7 @@ class BranchSchema(BaseModel):
# [/DEF:BranchSchema:Class]
# [DEF:CommitSchema:Class]
# @PURPOSE: Schema for representing Git commit details.
class CommitSchema(BaseModel):
"""Schema for representing a Git commit."""
hash: str
@@ -75,6 +81,7 @@ class CommitSchema(BaseModel):
# [/DEF:CommitSchema:Class]
# [DEF:BranchCreate:Class]
# @PURPOSE: Schema for branch creation requests.
class BranchCreate(BaseModel):
"""Schema for branch creation requests."""
name: str
@@ -82,12 +89,14 @@ class BranchCreate(BaseModel):
# [/DEF:BranchCreate:Class]
# [DEF:BranchCheckout:Class]
# @PURPOSE: Schema for branch checkout requests.
class BranchCheckout(BaseModel):
"""Schema for branch checkout requests."""
name: str
# [/DEF:BranchCheckout:Class]
# [DEF:CommitCreate:Class]
# @PURPOSE: Schema for staging and committing changes.
class CommitCreate(BaseModel):
"""Schema for staging and committing changes."""
message: str
@@ -95,6 +104,7 @@ class CommitCreate(BaseModel):
# [/DEF:CommitCreate:Class]
# [DEF:ConflictResolution:Class]
# @PURPOSE: Schema for resolving merge conflicts.
class ConflictResolution(BaseModel):
"""Schema for resolving merge conflicts."""
file_path: str
@@ -103,6 +113,7 @@ class ConflictResolution(BaseModel):
# [/DEF:ConflictResolution:Class]
# [DEF:DeploymentEnvironmentSchema:Class]
# @PURPOSE: Schema for representing a target deployment environment.
class DeploymentEnvironmentSchema(BaseModel):
"""Schema for representing a target deployment environment."""
id: str
@@ -115,12 +126,14 @@ class DeploymentEnvironmentSchema(BaseModel):
# [/DEF:DeploymentEnvironmentSchema:Class]
# [DEF:DeployRequest:Class]
# @PURPOSE: Schema for dashboard deployment requests.
class DeployRequest(BaseModel):
"""Schema for deployment requests."""
environment_id: str
# [/DEF:DeployRequest:Class]
# [DEF:RepoInitRequest:Class]
# @PURPOSE: Schema for repository initialization requests.
class RepoInitRequest(BaseModel):
"""Schema for repository initialization requests."""
config_id: str

View File

@@ -13,6 +13,7 @@
from fastapi import APIRouter, Depends, HTTPException
from typing import List
from ...core.config_models import AppConfig, Environment, GlobalSettings
from ...models.storage import StorageConfig
from ...dependencies import get_config_manager
from ...core.config_manager import ConfigManager
from ...core.logger import logger, belief_scope
@@ -52,10 +53,38 @@ async def update_global_settings(
):
with belief_scope("update_global_settings"):
logger.info("[update_global_settings][Entry] Updating global settings")
config_manager.update_global_settings(settings)
return settings
# [/DEF:update_global_settings:Function]
# [DEF:get_storage_settings:Function]
# @PURPOSE: Retrieves storage-specific settings.
# @RETURN: StorageConfig - The storage configuration.
@router.get("/storage", response_model=StorageConfig)
async def get_storage_settings(config_manager: ConfigManager = Depends(get_config_manager)):
with belief_scope("get_storage_settings"):
return config_manager.get_config().settings.storage
# [/DEF:get_storage_settings:Function]
# [DEF:update_storage_settings:Function]
# @PURPOSE: Updates storage-specific settings.
# @PARAM: storage (StorageConfig) - The new storage settings.
# @POST: Storage settings are updated and saved.
# @RETURN: StorageConfig - The updated storage settings.
@router.put("/storage", response_model=StorageConfig)
async def update_storage_settings(storage: StorageConfig, config_manager: ConfigManager = Depends(get_config_manager)):
with belief_scope("update_storage_settings"):
is_valid, message = config_manager.validate_path(storage.root_path)
if not is_valid:
raise HTTPException(status_code=400, detail=message)
settings = config_manager.get_config().settings
settings.storage = storage
config_manager.update_global_settings(settings)
return config_manager.get_config().settings.storage
# [/DEF:update_storage_settings:Function]
# [DEF:get_environments:Function]
# @PURPOSE: Lists all configured Superset environments.
# @PRE: Config manager is available.
@@ -179,30 +208,5 @@ async def test_environment_connection(
return {"status": "error", "message": str(e)}
# [/DEF:test_environment_connection:Function]
# [DEF:validate_backup_path:Function]
# @PURPOSE: Validates if a backup path exists and is writable.
# @PRE: Path is provided in path_data.
# @POST: Returns success or error status.
# @PARAM: path (str) - The path to validate.
# @RETURN: dict - Validation result.
@router.post("/validate-path")
async def validate_backup_path(
path_data: dict,
config_manager: ConfigManager = Depends(get_config_manager)
):
with belief_scope("validate_backup_path"):
path = path_data.get("path")
if not path:
raise HTTPException(status_code=400, detail="Path is required")
logger.info(f"[validate_backup_path][Entry] Validating path: {path}")
valid, message = config_manager.validate_path(path)
if not valid:
return {"status": "error", "message": message}
return {"status": "success", "message": message}
# [/DEF:validate_backup_path:Function]
# [/DEF:SettingsRouter:Module]

View File

@@ -0,0 +1,132 @@
# [DEF:storage_routes:Module]
#
# @SEMANTICS: storage, files, upload, download, backup, repository
# @PURPOSE: API endpoints for file storage management (backups and repositories).
# @LAYER: API
# @RELATION: DEPENDS_ON -> backend.src.models.storage
#
# @INVARIANT: All paths must be validated against path traversal.
# [SECTION: IMPORTS]
from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
from fastapi.responses import FileResponse
from typing import List, Optional
from ...models.storage import StoredFile, FileCategory
from ...dependencies import get_plugin_loader
from ...plugins.storage.plugin import StoragePlugin
from ...core.logger import belief_scope
# [/SECTION]
router = APIRouter(tags=["storage"])
# [DEF:list_files:Function]
# @PURPOSE: List all files and directories in the storage system.
#
# @PRE: None.
# @POST: Returns a list of StoredFile objects.
#
# @PARAM: category (Optional[FileCategory]) - Filter by category.
# @PARAM: path (Optional[str]) - Subpath within the category.
# @RETURN: List[StoredFile] - List of files/directories.
#
# @RELATION: CALLS -> StoragePlugin.list_files
@router.get("/files", response_model=List[StoredFile])
async def list_files(
category: Optional[FileCategory] = None,
path: Optional[str] = None,
plugin_loader=Depends(get_plugin_loader)
):
with belief_scope("list_files"):
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
if not storage_plugin:
raise HTTPException(status_code=500, detail="Storage plugin not loaded")
return storage_plugin.list_files(category, path)
# [/DEF:list_files:Function]
# [DEF:upload_file:Function]
# @PURPOSE: Upload a file to the storage system.
#
# @PRE: category must be a valid FileCategory.
# @PRE: file must be a valid UploadFile.
# @POST: Returns the StoredFile object of the uploaded file.
#
# @PARAM: category (FileCategory) - Target category.
# @PARAM: path (Optional[str]) - Target subpath.
# @PARAM: file (UploadFile) - The file content.
# @RETURN: StoredFile - Metadata of the uploaded file.
#
# @SIDE_EFFECT: Writes file to the filesystem.
#
# @RELATION: CALLS -> StoragePlugin.save_file
@router.post("/upload", response_model=StoredFile, status_code=201)
async def upload_file(
category: FileCategory = Form(...),
path: Optional[str] = Form(None),
file: UploadFile = File(...),
plugin_loader=Depends(get_plugin_loader)
):
with belief_scope("upload_file"):
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
if not storage_plugin:
raise HTTPException(status_code=500, detail="Storage plugin not loaded")
try:
return await storage_plugin.save_file(file, category, path)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# [/DEF:upload_file:Function]
# [DEF:delete_file:Function]
# @PURPOSE: Delete a specific file or directory.
#
# @PRE: category must be a valid FileCategory.
# @POST: Item is removed from storage.
#
# @PARAM: category (FileCategory) - File category.
# @PARAM: path (str) - Relative path of the item.
# @RETURN: None
#
# @SIDE_EFFECT: Deletes item from the filesystem.
#
# @RELATION: CALLS -> StoragePlugin.delete_file
@router.delete("/files/{category}/{path:path}", status_code=204)
async def delete_file(category: FileCategory, path: str, plugin_loader=Depends(get_plugin_loader)):
with belief_scope("delete_file"):
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
if not storage_plugin:
raise HTTPException(status_code=500, detail="Storage plugin not loaded")
try:
storage_plugin.delete_file(category, path)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="File not found")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# [/DEF:delete_file:Function]
# [DEF:download_file:Function]
# @PURPOSE: Retrieve a file for download.
#
# @PRE: category must be a valid FileCategory.
# @POST: Returns a FileResponse.
#
# @PARAM: category (FileCategory) - File category.
# @PARAM: path (str) - Relative path of the file.
# @RETURN: FileResponse - The file content.
#
# @RELATION: CALLS -> StoragePlugin.get_file_path
@router.get("/download/{category}/{path:path}")
async def download_file(category: FileCategory, path: str, plugin_loader=Depends(get_plugin_loader)):
with belief_scope("download_file"):
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
if not storage_plugin:
raise HTTPException(status_code=500, detail="Storage plugin not loaded")
try:
abs_path = storage_plugin.get_file_path(category, path)
filename = Path(path).name
return FileResponse(path=abs_path, filename=filename)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="File not found")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# [/DEF:download_file:Function]
# [/DEF:storage_routes:Module]

View File

@@ -18,7 +18,7 @@ import os
from .dependencies import get_task_manager, get_scheduler_service
from .core.logger import logger, belief_scope
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage
from .core.database import init_db
# [DEF:App:Global]
@@ -89,6 +89,7 @@ app.include_router(environments.router, prefix="/api/environments", tags=["Envir
app.include_router(mappings.router)
app.include_router(migration.router)
app.include_router(git.router)
app.include_router(storage.router, prefix="/api/storage", tags=["Storage"])
# [DEF:websocket_endpoint:Function]
# @PURPOSE: Provides a WebSocket endpoint for real-time log streaming of a task.

View File

@@ -62,14 +62,18 @@ class ConfigManager:
logger.info(f"[_load_config][Action] Config file not found. Creating default.")
default_config = AppConfig(
environments=[],
settings=GlobalSettings(backup_path="backups")
settings=GlobalSettings()
)
self._save_config_to_disk(default_config)
return default_config
try:
with open(self.config_path, "r") as f:
data = json.load(f)
# Check for deprecated field
if "settings" in data and "backup_path" in data["settings"]:
del data["settings"]["backup_path"]
config = AppConfig(**data)
logger.info(f"[_load_config][Coherence:OK] Configuration loaded")
return config
@@ -79,7 +83,7 @@ class ConfigManager:
# For now, return default to be safe, but log the error prominently.
return AppConfig(
environments=[],
settings=GlobalSettings(backup_path="backups")
settings=GlobalSettings(storage=StorageConfig())
)
# [/DEF:_load_config:Function]

View File

@@ -7,6 +7,7 @@
from pydantic import BaseModel, Field
from typing import List, Optional
from ..models.storage import StorageConfig
# [DEF:Schedule:DataClass]
# @PURPOSE: Represents a backup schedule configuration.
@@ -42,7 +43,7 @@ class LoggingConfig(BaseModel):
# [DEF:GlobalSettings:DataClass]
# @PURPOSE: Represents global application settings.
class GlobalSettings(BaseModel):
backup_path: str
storage: StorageConfig = Field(default_factory=StorageConfig)
default_environment_id: Optional[str] = None
logging: LoggingConfig = Field(default_factory=LoggingConfig)

View File

@@ -28,6 +28,7 @@ class BeliefFormatter(logging.Formatter):
# @POST: Returns formatted string.
# @PARAM: record (logging.LogRecord) - The log record to format.
# @RETURN: str - The formatted log message.
# @SEMANTICS: logging, formatter, context
def format(self, record):
anchor_id = getattr(_belief_state, 'anchor_id', None)
if anchor_id:
@@ -54,6 +55,7 @@ class LogEntry(BaseModel):
# @PARAM: message (str) - Optional entry message.
# @PRE: anchor_id must be provided.
# @POST: Thread-local belief state is updated and entry/exit logs are generated.
# @SEMANTICS: logging, context, belief_state
@contextmanager
def belief_scope(anchor_id: str, message: str = ""):
# Log Entry if enabled
@@ -88,6 +90,7 @@ def belief_scope(anchor_id: str, message: str = ""):
# @PRE: config is a valid LoggingConfig instance.
# @POST: Logger level, handlers, and belief state flag are updated.
# @PARAM: config (LoggingConfig) - The logging configuration.
# @SEMANTICS: logging, configuration, initialization
def configure_logger(config):
global _enable_belief_state
_enable_belief_state = config.enable_belief_state
@@ -140,6 +143,7 @@ class WebSocketLogHandler(logging.Handler):
# @PRE: capacity is an integer.
# @POST: Instance initialized with empty deque.
# @PARAM: capacity (int) - Maximum number of logs to keep in memory.
# @SEMANTICS: logging, initialization, buffer
def __init__(self, capacity: int = 1000):
super().__init__()
self.log_buffer: deque[LogEntry] = deque(maxlen=capacity)
@@ -152,6 +156,7 @@ class WebSocketLogHandler(logging.Handler):
# @PRE: record is a logging.LogRecord.
# @POST: Log is added to the log_buffer.
# @PARAM: record (logging.LogRecord) - The log record to emit.
# @SEMANTICS: logging, handler, buffer
def emit(self, record: logging.LogRecord):
try:
log_entry = LogEntry(
@@ -179,6 +184,7 @@ class WebSocketLogHandler(logging.Handler):
# @PRE: None.
# @POST: Returns list of LogEntry objects.
# @RETURN: List[LogEntry] - List of buffered log entries.
# @SEMANTICS: logging, buffer, retrieval
def get_recent_logs(self) -> List[LogEntry]:
"""
Returns a list of recent log entries from the buffer.
@@ -196,12 +202,24 @@ logger = logging.getLogger("superset_tools_app")
# [DEF:believed:Function]
# @PURPOSE: A decorator that wraps a function in a belief scope.
# @PARAM: anchor_id (str) - The identifier for the semantic block.
# @PRE: anchor_id must be a string.
# @POST: Returns a decorator function.
def believed(anchor_id: str):
# [DEF:decorator:Function]
# @PURPOSE: Internal decorator for belief scope.
# @PRE: func must be a callable.
# @POST: Returns the wrapped function.
def decorator(func):
# [DEF:wrapper:Function]
# @PURPOSE: Internal wrapper that enters belief scope.
# @PRE: None.
# @POST: Executes the function within a belief scope.
def wrapper(*args, **kwargs):
with belief_scope(anchor_id):
return func(*args, **kwargs)
# [/DEF:wrapper:Function]
return wrapper
# [/DEF:decorator:Function]
return decorator
# [/DEF:believed:Function]
logger.setLevel(logging.INFO)

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Dict, Any
from typing import Dict, Any, Optional
from .logger import belief_scope
from pydantic import BaseModel, Field
@@ -68,6 +68,21 @@ class PluginBase(ABC):
pass
# [/DEF:version:Function]
@property
# [DEF:ui_route:Function]
# @PURPOSE: Returns the frontend route for the plugin's UI, if applicable.
# @PRE: Plugin instance exists.
# @POST: Returns string route or None.
# @RETURN: Optional[str] - Frontend route.
def ui_route(self) -> Optional[str]:
"""
The frontend route for the plugin's UI.
Returns None if the plugin does not have a dedicated UI page.
"""
with belief_scope("ui_route"):
return None
# [/DEF:ui_route:Function]
@abstractmethod
# [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for the plugin's input parameters.
@@ -111,5 +126,6 @@ class PluginConfig(BaseModel):
name: str = Field(..., description="Human-readable name for the plugin")
description: str = Field(..., description="Brief description of what the plugin does")
version: str = Field(..., description="Version of the plugin")
ui_route: Optional[str] = Field(None, description="Frontend route for the plugin UI")
input_schema: Dict[str, Any] = Field(..., description="JSON schema for input parameters", alias="schema")
# [/DEF:PluginConfig:Class]

View File

@@ -50,9 +50,18 @@ class PluginLoader:
sys.path.insert(0, plugin_parent_dir)
for filename in os.listdir(self.plugin_dir):
file_path = os.path.join(self.plugin_dir, filename)
# Handle directory-based plugins (packages)
if os.path.isdir(file_path):
init_file = os.path.join(file_path, "__init__.py")
if os.path.exists(init_file):
self._load_module(filename, init_file)
continue
# Handle single-file plugins
if filename.endswith(".py") and filename != "__init__.py":
module_name = filename[:-3]
file_path = os.path.join(self.plugin_dir, filename)
self._load_module(module_name, file_path)
# [/DEF:_load_plugins:Function]
@@ -132,6 +141,7 @@ class PluginLoader:
name=plugin_instance.name,
description=plugin_instance.description,
version=plugin_instance.version,
ui_route=plugin_instance.ui_route,
schema=schema,
)
# The following line is commented out because it requires a schema to be passed to validate against.

View File

@@ -65,6 +65,8 @@ class SupersetClient:
@property
# [DEF:headers:Function]
# @PURPOSE: Возвращает базовые HTTP-заголовки, используемые сетевым клиентом.
# @PRE: APIClient is initialized and authenticated.
# @POST: Returns a dictionary of HTTP headers.
def headers(self) -> dict:
with belief_scope("headers"):
return self.network.headers
@@ -75,6 +77,8 @@ class SupersetClient:
# [DEF:get_dashboards:Function]
# @PURPOSE: Получает полный список дашбордов, автоматически обрабатывая пагинацию.
# @PARAM: query (Optional[Dict]) - Дополнительные параметры запроса для API.
# @PRE: Client is authenticated.
# @POST: Returns a tuple with total count and list of dashboards.
# @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список дашбордов).
def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
with belief_scope("get_dashboards"):
@@ -94,6 +98,8 @@ class SupersetClient:
# [DEF:get_dashboards_summary:Function]
# @PURPOSE: Fetches dashboard metadata optimized for the grid.
# @PRE: Client is authenticated.
# @POST: Returns a list of dashboard metadata summaries.
# @RETURN: List[Dict]
def get_dashboards_summary(self) -> List[Dict]:
with belief_scope("SupersetClient.get_dashboards_summary"):
@@ -117,6 +123,8 @@ class SupersetClient:
# [DEF:export_dashboard:Function]
# @PURPOSE: Экспортирует дашборд в виде ZIP-архива.
# @PARAM: dashboard_id (int) - ID дашборда для экспорта.
# @PRE: dashboard_id must exist in Superset.
# @POST: Returns ZIP content and filename.
# @RETURN: Tuple[bytes, str] - Бинарное содержимое ZIP-архива и имя файла.
def export_dashboard(self, dashboard_id: int) -> Tuple[bytes, str]:
with belief_scope("export_dashboard"):
@@ -140,6 +148,8 @@ class SupersetClient:
# @PARAM: file_name (Union[str, Path]) - Путь к ZIP-архиву.
# @PARAM: dash_id (Optional[int]) - ID дашборда для удаления при сбое.
# @PARAM: dash_slug (Optional[str]) - Slug дашборда для поиска ID.
# @PRE: file_name must be a valid ZIP dashboard export.
# @POST: Dashboard is imported or re-imported after deletion.
# @RETURN: Dict - Ответ API в случае успеха.
def import_dashboard(self, file_name: Union[str, Path], dash_id: Optional[int] = None, dash_slug: Optional[str] = None) -> Dict:
with belief_scope("import_dashboard"):
@@ -165,6 +175,8 @@ class SupersetClient:
# [DEF:delete_dashboard:Function]
# @PURPOSE: Удаляет дашборд по его ID или slug.
# @PARAM: dashboard_id (Union[int, str]) - ID или slug дашборда.
# @PRE: dashboard_id must exist.
# @POST: Dashboard is removed from Superset.
def delete_dashboard(self, dashboard_id: Union[int, str]) -> None:
with belief_scope("delete_dashboard"):
app_logger.info("[delete_dashboard][Enter] Deleting dashboard %s.", dashboard_id)
@@ -183,6 +195,8 @@ class SupersetClient:
# [DEF:get_datasets:Function]
# @PURPOSE: Получает полный список датасетов, автоматически обрабатывая пагинацию.
# @PARAM: query (Optional[Dict]) - Дополнительные параметры запроса.
# @PRE: Client is authenticated.
# @POST: Returns total count and list of datasets.
# @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список датасетов).
def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
with belief_scope("get_datasets"):
@@ -201,6 +215,8 @@ class SupersetClient:
# [DEF:get_dataset:Function]
# @PURPOSE: Получает информацию о конкретном датасете по его ID.
# @PARAM: dataset_id (int) - ID датасета.
# @PRE: dataset_id must exist.
# @POST: Returns dataset details.
# @RETURN: Dict - Информация о датасете.
def get_dataset(self, dataset_id: int) -> Dict:
with belief_scope("SupersetClient.get_dataset", f"id={dataset_id}"):
@@ -215,6 +231,8 @@ class SupersetClient:
# @PURPOSE: Обновляет данные датасета по его ID.
# @PARAM: dataset_id (int) - ID датасета.
# @PARAM: data (Dict) - Данные для обновления.
# @PRE: dataset_id must exist.
# @POST: Dataset is updated in Superset.
# @RETURN: Dict - Ответ API.
def update_dataset(self, dataset_id: int, data: Dict) -> Dict:
with belief_scope("SupersetClient.update_dataset", f"id={dataset_id}"):
@@ -237,6 +255,8 @@ class SupersetClient:
# [DEF:get_databases:Function]
# @PURPOSE: Получает полный список баз данных.
# @PARAM: query (Optional[Dict]) - Дополнительные параметры запроса.
# @PRE: Client is authenticated.
# @POST: Returns total count and list of databases.
# @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список баз данных).
def get_databases(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
with belief_scope("get_databases"):
@@ -256,6 +276,8 @@ class SupersetClient:
# [DEF:get_database:Function]
# @PURPOSE: Получает информацию о конкретной базе данных по её ID.
# @PARAM: database_id (int) - ID базы данных.
# @PRE: database_id must exist.
# @POST: Returns database details.
# @RETURN: Dict - Информация о базе данных.
def get_database(self, database_id: int) -> Dict:
with belief_scope("get_database"):
@@ -268,6 +290,8 @@ class SupersetClient:
# [DEF:get_databases_summary:Function]
# @PURPOSE: Fetch a summary of databases including uuid, name, and engine.
# @PRE: Client is authenticated.
# @POST: Returns list of database summaries.
# @RETURN: List[Dict] - Summary of databases.
def get_databases_summary(self) -> List[Dict]:
with belief_scope("SupersetClient.get_databases_summary"):
@@ -286,6 +310,8 @@ class SupersetClient:
# [DEF:get_database_by_uuid:Function]
# @PURPOSE: Find a database by its UUID.
# @PARAM: db_uuid (str) - The UUID of the database.
# @PRE: db_uuid must be a valid UUID string.
# @POST: Returns database info or None.
# @RETURN: Optional[Dict] - Database info if found, else None.
def get_database_by_uuid(self, db_uuid: str) -> Optional[Dict]:
with belief_scope("SupersetClient.get_database_by_uuid", f"uuid={db_uuid}"):
@@ -301,6 +327,9 @@ class SupersetClient:
# [SECTION: HELPERS]
# [DEF:_resolve_target_id_for_delete:Function]
# @PURPOSE: Resolves a dashboard ID from either an ID or a slug.
# @PRE: Either dash_id or dash_slug should be provided.
# @POST: Returns the resolved ID or None.
def _resolve_target_id_for_delete(self, dash_id: Optional[int], dash_slug: Optional[str]) -> Optional[int]:
with belief_scope("_resolve_target_id_for_delete"):
if dash_id is not None:
@@ -319,6 +348,9 @@ class SupersetClient:
# [/DEF:_resolve_target_id_for_delete:Function]
# [DEF:_do_import:Function]
# @PURPOSE: Performs the actual multipart upload for import.
# @PRE: file_name must be a path to an existing ZIP file.
# @POST: Returns the API response from the upload.
def _do_import(self, file_name: Union[str, Path]) -> Dict:
with belief_scope("_do_import"):
app_logger.debug(f"[_do_import][State] Uploading file: {file_name}")
@@ -336,6 +368,9 @@ class SupersetClient:
# [/DEF:_do_import:Function]
# [DEF:_validate_export_response:Function]
# @PURPOSE: Validates that the export response is a non-empty ZIP archive.
# @PRE: response must be a valid requests.Response object.
# @POST: Raises SupersetAPIError if validation fails.
def _validate_export_response(self, response: Response, dashboard_id: int) -> None:
with belief_scope("_validate_export_response"):
content_type = response.headers.get("Content-Type", "")
@@ -346,6 +381,9 @@ class SupersetClient:
# [/DEF:_validate_export_response:Function]
# [DEF:_resolve_export_filename:Function]
# @PURPOSE: Determines the filename for an exported dashboard.
# @PRE: response must contain Content-Disposition header or dashboard_id must be provided.
# @POST: Returns a sanitized filename string.
def _resolve_export_filename(self, response: Response, dashboard_id: int) -> str:
with belief_scope("_resolve_export_filename"):
filename = get_filename_from_headers(dict(response.headers))
@@ -358,6 +396,9 @@ class SupersetClient:
# [/DEF:_resolve_export_filename:Function]
# [DEF:_validate_query_params:Function]
# @PURPOSE: Ensures query parameters have default page and page_size.
# @PRE: query can be None or a dictionary.
# @POST: Returns a dictionary with at least page and page_size.
def _validate_query_params(self, query: Optional[Dict]) -> Dict:
with belief_scope("_validate_query_params"):
base_query = {"page": 0, "page_size": 1000}
@@ -365,6 +406,9 @@ class SupersetClient:
# [/DEF:_validate_query_params:Function]
# [DEF:_fetch_total_object_count:Function]
# @PURPOSE: Fetches the total number of items for a given endpoint.
# @PRE: endpoint must be a valid Superset API path.
# @POST: Returns the total count as an integer.
def _fetch_total_object_count(self, endpoint: str) -> int:
with belief_scope("_fetch_total_object_count"):
return self.network.fetch_paginated_count(
@@ -375,12 +419,18 @@ class SupersetClient:
# [/DEF:_fetch_total_object_count:Function]
# [DEF:_fetch_all_pages:Function]
# @PURPOSE: Iterates through all pages to collect all data items.
# @PRE: pagination_options must contain base_query, total_count, and results_field.
# @POST: Returns a combined list of all items.
def _fetch_all_pages(self, endpoint: str, pagination_options: Dict) -> List[Dict]:
with belief_scope("_fetch_all_pages"):
return self.network.fetch_paginated_data(endpoint=endpoint, pagination_options=pagination_options)
# [/DEF:_fetch_all_pages:Function]
# [DEF:_validate_import_file:Function]
# @PURPOSE: Validates that the file to be imported is a valid ZIP with metadata.yaml.
# @PRE: zip_path must be a path to a file.
# @POST: Raises error if file is missing, not a ZIP, or missing metadata.
def _validate_import_file(self, zip_path: Union[str, Path]) -> None:
with belief_scope("_validate_import_file"):
path = Path(zip_path)

View File

@@ -24,8 +24,10 @@ from ..logger import logger as app_logger, belief_scope
# [/SECTION]
# [DEF:InvalidZipFormatError:Class]
# @PURPOSE: Exception raised when a file is not a valid ZIP archive.
class InvalidZipFormatError(Exception):
pass
# [/DEF:InvalidZipFormatError:Class]
# [DEF:create_temp_file:Function]
# @PURPOSE: Контекстный менеджер для создания временного файла или директории с гарантированным удалением.

View File

@@ -20,31 +20,71 @@ from ..logger import logger as app_logger, belief_scope
# [/SECTION]
# [DEF:SupersetAPIError:Class]
# @PURPOSE: Base exception for all Superset API related errors.
class SupersetAPIError(Exception):
# [DEF:__init__:Function]
# @PURPOSE: Initializes the exception with a message and context.
# @PRE: message is a string, context is a dict.
# @POST: Exception is initialized with context.
def __init__(self, message: str = "Superset API error", **context: Any):
self.context = context
super().__init__(f"[API_FAILURE] {message} | Context: {self.context}")
with belief_scope("SupersetAPIError.__init__"):
self.context = context
super().__init__(f"[API_FAILURE] {message} | Context: {self.context}")
# [/DEF:__init__:Function]
# [/DEF:SupersetAPIError:Class]
# [DEF:AuthenticationError:Class]
# @PURPOSE: Exception raised when authentication fails.
class AuthenticationError(SupersetAPIError):
# [DEF:__init__:Function]
# @PURPOSE: Initializes the authentication error.
# @PRE: message is a string, context is a dict.
# @POST: AuthenticationError is initialized.
def __init__(self, message: str = "Authentication failed", **context: Any):
super().__init__(message, type="authentication", **context)
with belief_scope("AuthenticationError.__init__"):
super().__init__(message, type="authentication", **context)
# [/DEF:__init__:Function]
# [/DEF:AuthenticationError:Class]
# [DEF:PermissionDeniedError:Class]
# @PURPOSE: Exception raised when access is denied.
class PermissionDeniedError(AuthenticationError):
# [DEF:__init__:Function]
# @PURPOSE: Initializes the permission denied error.
# @PRE: message is a string, context is a dict.
# @POST: PermissionDeniedError is initialized.
def __init__(self, message: str = "Permission denied", **context: Any):
super().__init__(message, **context)
with belief_scope("PermissionDeniedError.__init__"):
super().__init__(message, **context)
# [/DEF:__init__:Function]
# [/DEF:PermissionDeniedError:Class]
# [DEF:DashboardNotFoundError:Class]
# @PURPOSE: Exception raised when a dashboard cannot be found.
class DashboardNotFoundError(SupersetAPIError):
# [DEF:__init__:Function]
# @PURPOSE: Initializes the not found error with resource ID.
# @PRE: resource_id is provided.
# @POST: DashboardNotFoundError is initialized.
def __init__(self, resource_id: Union[int, str], message: str = "Dashboard not found", **context: Any):
super().__init__(f"Dashboard '{resource_id}' {message}", subtype="not_found", resource_id=resource_id, **context)
with belief_scope("DashboardNotFoundError.__init__"):
super().__init__(f"Dashboard '{resource_id}' {message}", subtype="not_found", resource_id=resource_id, **context)
# [/DEF:__init__:Function]
# [/DEF:DashboardNotFoundError:Class]
# [DEF:NetworkError:Class]
# @PURPOSE: Exception raised when a network level error occurs.
class NetworkError(Exception):
# [DEF:__init__:Function]
# @PURPOSE: Initializes the network error.
# @PRE: message is a string.
# @POST: NetworkError is initialized.
def __init__(self, message: str = "Network connection failed", **context: Any):
self.context = context
super().__init__(f"[NETWORK_FAILURE] {message} | Context: {self.context}")
with belief_scope("NetworkError.__init__"):
self.context = context
super().__init__(f"[NETWORK_FAILURE] {message} | Context: {self.context}")
# [/DEF:__init__:Function]
# [/DEF:NetworkError:Class]
# [DEF:APIClient:Class]
# @PURPOSE: Инкапсулирует HTTP-логику для работы с API, включая сессии, аутентификацию, и обработку запросов.

View File

@@ -1,8 +1,8 @@
"""
[DEF:GitModels:Module]
Git-specific SQLAlchemy models for configuration and repository tracking.
@RELATION: specs/011-git-integration-dashboard/data-model.md
"""
# [DEF:GitModels:Module]
# @SEMANTICS: git, models, sqlalchemy, database, schema
# @PURPOSE: Git-specific SQLAlchemy models for configuration and repository tracking.
# @LAYER: Model
# @RELATION: specs/011-git-integration-dashboard/data-model.md
import enum
from datetime import datetime

View File

@@ -0,0 +1,31 @@
from datetime import datetime
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
# [DEF:FileCategory:Class]
# @PURPOSE: Enumeration of supported file categories in the storage system.
class FileCategory(str, Enum):
BACKUP = "backups"
REPOSITORY = "repositorys"
# [/DEF:FileCategory:Class]
# [DEF:StorageConfig:Class]
# @PURPOSE: Configuration model for the storage system, defining paths and naming patterns.
class StorageConfig(BaseModel):
root_path: str = Field(default="backups", description="Absolute path to the storage root directory.")
backup_structure_pattern: str = Field(default="{category}/", description="Pattern for backup directory structure.")
repo_structure_pattern: str = Field(default="{category}/", description="Pattern for repository directory structure.")
filename_pattern: str = Field(default="{name}_{timestamp}", description="Pattern for filenames.")
# [/DEF:StorageConfig:Class]
# [DEF:StoredFile:Class]
# @PURPOSE: Data model representing metadata for a file stored in the system.
class StoredFile(BaseModel):
name: str = Field(..., description="Name of the file (including extension).")
path: str = Field(..., description="Relative path from storage root.")
size: int = Field(..., ge=0, description="Size of the file in bytes.")
created_at: datetime = Field(..., description="Creation timestamp.")
category: FileCategory = Field(..., description="Category of the file.")
mime_type: Optional[str] = Field(None, description="MIME type of the file.")
# [/DEF:StoredFile:Class]

View File

@@ -75,6 +75,15 @@ class BackupPlugin(PluginBase):
return "1.0.0"
# [/DEF:version:Function]
@property
# [DEF:ui_route:Function]
# @PURPOSE: Returns the frontend route for the backup plugin.
# @RETURN: str - "/tools/backups"
def ui_route(self) -> str:
with belief_scope("ui_route"):
return "/tools/backups"
# [/DEF:ui_route:Function]
# [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for backup plugin parameters.
# @PRE: Plugin instance exists.
@@ -84,7 +93,7 @@ class BackupPlugin(PluginBase):
with belief_scope("get_schema"):
config_manager = get_config_manager()
envs = [e.name for e in config_manager.get_environments()]
default_path = config_manager.get_config().settings.backup_path
default_path = config_manager.get_config().settings.storage.root_path
return {
"type": "object",
@@ -95,14 +104,8 @@ class BackupPlugin(PluginBase):
"description": "The Superset environment to back up.",
"enum": envs if envs else [],
},
"backup_path": {
"type": "string",
"title": "Backup Path",
"description": "The root directory to save backups to.",
"default": default_path
}
},
"required": ["env", "backup_path"],
"required": ["env"],
}
# [/DEF:get_schema:Function]
@@ -126,8 +129,9 @@ class BackupPlugin(PluginBase):
if not env:
raise KeyError("env")
backup_path_str = params.get("backup_path") or config_manager.get_config().settings.backup_path
backup_path = Path(backup_path_str)
storage_settings = config_manager.get_config().settings.storage
# Use 'backups' subfolder within the storage root
backup_path = Path(storage_settings.root_path) / "backups"
from ..core.logger import logger as app_logger
app_logger.info(f"[BackupPlugin][Entry] Starting backup for {env}.")

View File

@@ -63,6 +63,15 @@ class DebugPlugin(PluginBase):
return "1.0.0"
# [/DEF:version:Function]
@property
# [DEF:ui_route:Function]
# @PURPOSE: Returns the frontend route for the debug plugin.
# @RETURN: str - "/tools/debug"
def ui_route(self) -> str:
with belief_scope("ui_route"):
return "/tools/debug"
# [/DEF:ui_route:Function]
# [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for the debug plugin parameters.
# @PRE: Plugin instance exists.

View File

@@ -31,6 +31,7 @@ class GitPlugin(PluginBase):
# [DEF:__init__:Function]
# @PURPOSE: Инициализирует плагин и его зависимости.
# @PRE: config.json exists or shared config_manager is available.
# @POST: Инициализированы git_service и config_manager.
def __init__(self):
with belief_scope("GitPlugin.__init__"):
@@ -59,23 +60,58 @@ class GitPlugin(PluginBase):
# [/DEF:__init__:Function]
@property
# [DEF:id:Function]
# @PURPOSE: Returns the plugin identifier.
# @PRE: GitPlugin is initialized.
# @POST: Returns 'git-integration'.
def id(self) -> str:
return "git-integration"
with belief_scope("GitPlugin.id"):
return "git-integration"
# [/DEF:id:Function]
@property
# [DEF:name:Function]
# @PURPOSE: Returns the plugin name.
# @PRE: GitPlugin is initialized.
# @POST: Returns the human-readable name.
def name(self) -> str:
return "Git Integration"
with belief_scope("GitPlugin.name"):
return "Git Integration"
# [/DEF:name:Function]
@property
# [DEF:description:Function]
# @PURPOSE: Returns the plugin description.
# @PRE: GitPlugin is initialized.
# @POST: Returns the plugin's purpose description.
def description(self) -> str:
return "Version control for Superset dashboards"
with belief_scope("GitPlugin.description"):
return "Version control for Superset dashboards"
# [/DEF:description:Function]
@property
# [DEF:version:Function]
# @PURPOSE: Returns the plugin version.
# @PRE: GitPlugin is initialized.
# @POST: Returns the version string.
def version(self) -> str:
return "0.1.0"
with belief_scope("GitPlugin.version"):
return "0.1.0"
# [/DEF:version:Function]
@property
# [DEF:ui_route:Function]
# @PURPOSE: Returns the frontend route for the git plugin.
# @RETURN: str - "/git"
def ui_route(self) -> str:
with belief_scope("GitPlugin.ui_route"):
return "/git"
# [/DEF:ui_route:Function]
# [DEF:get_schema:Function]
# @PURPOSE: Возвращает JSON-схему параметров для выполнения задач плагина.
# @PRE: GitPlugin is initialized.
# @POST: Returns a JSON schema dictionary.
# @RETURN: Dict[str, Any] - Схема параметров.
def get_schema(self) -> Dict[str, Any]:
with belief_scope("GitPlugin.get_schema"):
@@ -93,6 +129,7 @@ class GitPlugin(PluginBase):
# [DEF:initialize:Function]
# @PURPOSE: Выполняет начальную настройку плагина.
# @PRE: GitPlugin is initialized.
# @POST: Плагин готов к выполнению задач.
async def initialize(self):
with belief_scope("GitPlugin.initialize"):
@@ -281,6 +318,8 @@ class GitPlugin(PluginBase):
# [DEF:_get_env:Function]
# @PURPOSE: Вспомогательный метод для получения конфигурации окружения.
# @PARAM: env_id (Optional[str]) - ID окружения.
# @PRE: env_id is a string or None.
# @POST: Returns an Environment object from config or DB.
# @RETURN: Environment - Объект конфигурации окружения.
def _get_env(self, env_id: Optional[str] = None):
with belief_scope("GitPlugin._get_env"):
@@ -341,5 +380,6 @@ class GitPlugin(PluginBase):
raise ValueError("No environments configured. Please add a Superset Environment in Settings.")
# [/DEF:_get_env:Function]
# [/DEF:initialize:Function]
# [/DEF:GitPlugin:Class]
# [/DEF:backend.src.plugins.git_plugin:Module]

View File

@@ -66,6 +66,15 @@ class MapperPlugin(PluginBase):
return "1.0.0"
# [/DEF:version:Function]
@property
# [DEF:ui_route:Function]
# @PURPOSE: Returns the frontend route for the mapper plugin.
# @RETURN: str - "/tools/mapper"
def ui_route(self) -> str:
with belief_scope("ui_route"):
return "/tools/mapper"
# [/DEF:ui_route:Function]
# [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for the mapper plugin parameters.
# @PRE: Plugin instance exists.

View File

@@ -71,6 +71,15 @@ class MigrationPlugin(PluginBase):
return "1.0.0"
# [/DEF:version:Function]
@property
# [DEF:ui_route:Function]
# @PURPOSE: Returns the frontend route for the migration plugin.
# @RETURN: str - "/migration"
def ui_route(self) -> str:
with belief_scope("ui_route"):
return "/migration"
# [/DEF:ui_route:Function]
# [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for migration plugin parameters.
# @PRE: Config manager is available.

View File

@@ -64,6 +64,15 @@ class SearchPlugin(PluginBase):
return "1.0.0"
# [/DEF:version:Function]
@property
# [DEF:ui_route:Function]
# @PURPOSE: Returns the frontend route for the search plugin.
# @RETURN: str - "/tools/search"
def ui_route(self) -> str:
with belief_scope("ui_route"):
return "/tools/search"
# [/DEF:ui_route:Function]
# [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for the search plugin parameters.
# @PRE: Plugin instance exists.

View File

@@ -0,0 +1,3 @@
from .plugin import StoragePlugin
__all__ = ["StoragePlugin"]

View File

@@ -0,0 +1,333 @@
# [DEF:StoragePlugin:Module]
#
# @SEMANTICS: storage, files, filesystem, plugin
# @PURPOSE: Provides core filesystem operations for managing backups and repositories.
# @LAYER: App
# @RELATION: IMPLEMENTS -> PluginBase
# @RELATION: DEPENDS_ON -> backend.src.models.storage
#
# @INVARIANT: All file operations must be restricted to the configured storage root.
# [SECTION: IMPORTS]
import os
import shutil
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, List, Optional
from fastapi import UploadFile
from ...core.plugin_base import PluginBase
from ...core.logger import belief_scope, logger
from ...models.storage import StoredFile, FileCategory, StorageConfig
from ...dependencies import get_config_manager
# [/SECTION]
# [DEF:StoragePlugin:Class]
# @PURPOSE: Implementation of the storage management plugin.
class StoragePlugin(PluginBase):
"""
Plugin for managing local file storage for backups and repositories.
"""
# [DEF:__init__:Function]
# @PURPOSE: Initializes the StoragePlugin and ensures required directories exist.
# @PRE: Configuration manager must be accessible.
# @POST: Storage root and category directories are created on disk.
def __init__(self):
with belief_scope("StoragePlugin:init"):
self.ensure_directories()
# [/DEF:__init__:Function]
@property
# [DEF:id:Function]
# @PURPOSE: Returns the unique identifier for the storage plugin.
# @PRE: None.
# @POST: Returns the plugin ID string.
# @RETURN: str - "storage-manager"
def id(self) -> str:
with belief_scope("StoragePlugin:id"):
return "storage-manager"
# [/DEF:id:Function]
@property
# [DEF:name:Function]
# @PURPOSE: Returns the human-readable name of the storage plugin.
# @PRE: None.
# @POST: Returns the plugin name string.
# @RETURN: str - "Storage Manager"
def name(self) -> str:
with belief_scope("StoragePlugin:name"):
return "Storage Manager"
# [/DEF:name:Function]
@property
# [DEF:description:Function]
# @PURPOSE: Returns a description of the storage plugin.
# @PRE: None.
# @POST: Returns the plugin description string.
# @RETURN: str - Plugin description.
def description(self) -> str:
with belief_scope("StoragePlugin:description"):
return "Manages local file storage for backups and repositories."
# [/DEF:description:Function]
@property
# [DEF:version:Function]
# @PURPOSE: Returns the version of the storage plugin.
# @PRE: None.
# @POST: Returns the version string.
# @RETURN: str - "1.0.0"
def version(self) -> str:
with belief_scope("StoragePlugin:version"):
return "1.0.0"
# [/DEF:version:Function]
@property
# [DEF:ui_route:Function]
# @PURPOSE: Returns the frontend route for the storage plugin.
# @RETURN: str - "/tools/storage"
def ui_route(self) -> str:
with belief_scope("StoragePlugin:ui_route"):
return "/tools/storage"
# [/DEF:ui_route:Function]
# [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for storage plugin parameters.
# @PRE: None.
# @POST: Returns a dictionary representing the JSON schema.
# @RETURN: Dict[str, Any] - JSON schema.
def get_schema(self) -> Dict[str, Any]:
with belief_scope("StoragePlugin:get_schema"):
return {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": [c.value for c in FileCategory],
"title": "Category"
}
},
"required": ["category"]
}
# [/DEF:get_schema:Function]
# [DEF:execute:Function]
# @PURPOSE: Executes storage-related tasks (placeholder for PluginBase compliance).
# @PRE: params must match the plugin schema.
# @POST: Task is executed and logged.
async def execute(self, params: Dict[str, Any]):
with belief_scope("StoragePlugin:execute"):
logger.info(f"[StoragePlugin][Action] Executing with params: {params}")
# [/DEF:execute:Function]
# [DEF:get_storage_root:Function]
# @PURPOSE: Resolves the absolute path to the storage root.
# @PRE: Settings must define a storage root path.
# @POST: Returns a Path object representing the storage root.
def get_storage_root(self) -> Path:
with belief_scope("StoragePlugin:get_storage_root"):
config_manager = get_config_manager()
global_settings = config_manager.get_config().settings
# Use storage.root_path as the source of truth for storage UI
root = Path(global_settings.storage.root_path)
if not root.is_absolute():
# Resolve relative to the backend directory
# Path(__file__) is backend/src/plugins/storage/plugin.py
# parents[3] is the project root (ss-tools)
# We need to ensure it's relative to where backend/ is
project_root = Path(__file__).parents[3]
root = (project_root / root).resolve()
return root
# [/DEF:get_storage_root:Function]
# [DEF:resolve_path:Function]
# @PURPOSE: Resolves a dynamic path pattern using provided variables.
# @PARAM: pattern (str) - The path pattern to resolve.
# @PARAM: variables (Dict[str, str]) - Variables to substitute in the pattern.
# @PRE: pattern must be a valid format string.
# @POST: Returns the resolved path string.
# @RETURN: str - The resolved path.
def resolve_path(self, pattern: str, variables: Dict[str, str]) -> str:
with belief_scope("StoragePlugin:resolve_path"):
# Add common variables
vars_with_defaults = {
"timestamp": datetime.now().strftime("%Y%m%dT%H%M%S"),
**variables
}
try:
resolved = pattern.format(**vars_with_defaults)
# Clean up any double slashes or leading/trailing slashes for relative path
return os.path.normpath(resolved).strip("/")
except KeyError as e:
logger.warning(f"[StoragePlugin][Coherence:Failed] Missing variable for path resolution: {e}")
# Fallback to literal pattern if formatting fails partially (or handle as needed)
return pattern.replace("{", "").replace("}", "")
# [/DEF:resolve_path:Function]
# [DEF:ensure_directories:Function]
# @PURPOSE: Creates the storage root and category subdirectories if they don't exist.
# @PRE: Storage root must be resolvable.
# @POST: Directories are created on the filesystem.
# @SIDE_EFFECT: Creates directories on the filesystem.
def ensure_directories(self):
with belief_scope("StoragePlugin:ensure_directories"):
root = self.get_storage_root()
for category in FileCategory:
# Use singular name for consistency with BackupPlugin and GitService
path = root / category.value
path.mkdir(parents=True, exist_ok=True)
logger.debug(f"[StoragePlugin][Action] Ensured directory: {path}")
# [/DEF:ensure_directories:Function]
# [DEF:validate_path:Function]
# @PURPOSE: Prevents path traversal attacks by ensuring the path is within the storage root.
# @PRE: path must be a Path object.
# @POST: Returns the resolved absolute path if valid, otherwise raises ValueError.
def validate_path(self, path: Path) -> Path:
with belief_scope("StoragePlugin:validate_path"):
root = self.get_storage_root().resolve()
resolved = path.resolve()
try:
resolved.relative_to(root)
except ValueError:
logger.error(f"[StoragePlugin][Coherence:Failed] Path traversal detected: {resolved} is not under {root}")
raise ValueError("Access denied: Path is outside of storage root.")
return resolved
# [/DEF:validate_path:Function]
# [DEF:list_files:Function]
# @PURPOSE: Lists all files and directories in a specific category and subpath.
# @PARAM: category (Optional[FileCategory]) - The category to list.
# @PARAM: subpath (Optional[str]) - Nested path within the category.
# @PRE: Storage root must exist.
# @POST: Returns a list of StoredFile objects.
# @RETURN: List[StoredFile] - List of file and directory metadata objects.
def list_files(self, category: Optional[FileCategory] = None, subpath: Optional[str] = None) -> List[StoredFile]:
with belief_scope("StoragePlugin:list_files"):
root = self.get_storage_root()
logger.info(f"[StoragePlugin][Action] Listing files in root: {root}, category: {category}, subpath: {subpath}")
files = []
categories = [category] if category else list(FileCategory)
for cat in categories:
# Scan the category subfolder + optional subpath
base_dir = root / cat.value
if subpath:
target_dir = self.validate_path(base_dir / subpath)
else:
target_dir = base_dir
if not target_dir.exists():
continue
logger.debug(f"[StoragePlugin][Action] Scanning directory: {target_dir}")
# Use os.scandir for better performance and to distinguish files vs dirs
with os.scandir(target_dir) as it:
for entry in it:
# Skip logs
if "Logs" in entry.path:
continue
stat = entry.stat()
is_dir = entry.is_dir()
files.append(StoredFile(
name=entry.name,
path=str(Path(entry.path).relative_to(root)),
size=stat.st_size if not is_dir else 0,
created_at=datetime.fromtimestamp(stat.st_ctime),
category=cat,
mime_type="directory" if is_dir else None
))
# Sort: directories first, then by name
return sorted(files, key=lambda x: (x.mime_type != "directory", x.name))
# [/DEF:list_files:Function]
# [DEF:save_file:Function]
# @PURPOSE: Saves an uploaded file to the specified category and optional subpath.
# @PARAM: file (UploadFile) - The uploaded file.
# @PARAM: category (FileCategory) - The target category.
# @PARAM: subpath (Optional[str]) - The target subpath.
# @PRE: file must be a valid UploadFile; category must be valid.
# @POST: File is written to disk and metadata is returned.
# @RETURN: StoredFile - Metadata of the saved file.
# @SIDE_EFFECT: Writes file to disk.
async def save_file(self, file: UploadFile, category: FileCategory, subpath: Optional[str] = None) -> StoredFile:
with belief_scope("StoragePlugin:save_file"):
root = self.get_storage_root()
dest_dir = root / category.value
if subpath:
dest_dir = dest_dir / subpath
dest_dir.mkdir(parents=True, exist_ok=True)
dest_path = self.validate_path(dest_dir / file.filename)
with dest_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
stat = dest_path.stat()
return StoredFile(
name=dest_path.name,
path=str(dest_path.relative_to(root)),
size=stat.st_size,
created_at=datetime.fromtimestamp(stat.st_ctime),
category=category,
mime_type=file.content_type
)
# [/DEF:save_file:Function]
# [DEF:delete_file:Function]
# @PURPOSE: Deletes a file or directory from the specified category and path.
# @PARAM: category (FileCategory) - The category.
# @PARAM: path (str) - The relative path of the file or directory.
# @PRE: path must belong to the specified category and exist on disk.
# @POST: The file or directory is removed from disk.
# @SIDE_EFFECT: Removes item from disk.
def delete_file(self, category: FileCategory, path: str):
with belief_scope("StoragePlugin:delete_file"):
root = self.get_storage_root()
# path is relative to root, but we ensure it starts with category
full_path = self.validate_path(root / path)
if not str(Path(path)).startswith(category.value):
raise ValueError(f"Path {path} does not belong to category {category}")
if full_path.exists():
if full_path.is_dir():
shutil.rmtree(full_path)
else:
full_path.unlink()
logger.info(f"[StoragePlugin][Action] Deleted: {full_path}")
else:
raise FileNotFoundError(f"Item {path} not found")
# [/DEF:delete_file:Function]
# [DEF:get_file_path:Function]
# @PURPOSE: Returns the absolute path of a file for download.
# @PARAM: category (FileCategory) - The category.
# @PARAM: path (str) - The relative path of the file.
# @PRE: path must belong to the specified category and be a file.
# @POST: Returns the absolute Path to the file.
# @RETURN: Path - Absolute path to the file.
def get_file_path(self, category: FileCategory, path: str) -> Path:
with belief_scope("StoragePlugin:get_file_path"):
root = self.get_storage_root()
file_path = self.validate_path(root / path)
if not str(Path(path)).startswith(category.value):
raise ValueError(f"Path {path} does not belong to category {category}")
if not file_path.exists() or file_path.is_dir():
raise FileNotFoundError(f"File {path} not found")
return file_path
# [/DEF:get_file_path:Function]
# [/DEF:StoragePlugin:Class]
# [/DEF:StoragePlugin:Module]

View File

@@ -29,9 +29,17 @@ class GitService:
# [DEF:__init__:Function]
# @PURPOSE: Initializes the GitService with a base path for repositories.
# @PARAM: base_path (str) - Root directory for all Git clones.
def __init__(self, base_path: str = "backend/git_repos"):
# @PRE: base_path is a valid string path.
# @POST: GitService is initialized; base_path directory exists.
def __init__(self, base_path: str = "git_repos"):
with belief_scope("GitService.__init__"):
self.base_path = base_path
# Resolve relative to the backend directory
# Path(__file__) is backend/src/services/git_service.py
# parents[2] is backend/
from pathlib import Path
backend_root = Path(__file__).parents[2]
self.base_path = str((backend_root / base_path).resolve())
if not os.path.exists(self.base_path):
os.makedirs(self.base_path)
# [/DEF:__init__:Function]
@@ -39,9 +47,12 @@ class GitService:
# [DEF:_get_repo_path:Function]
# @PURPOSE: Resolves the local filesystem path for a dashboard's repository.
# @PARAM: dashboard_id (int)
# @PRE: dashboard_id is an integer.
# @POST: Returns the absolute or relative path to the dashboard's repo.
# @RETURN: str
def _get_repo_path(self, dashboard_id: int) -> str:
return os.path.join(self.base_path, str(dashboard_id))
with belief_scope("GitService._get_repo_path"):
return os.path.join(self.base_path, str(dashboard_id))
# [/DEF:_get_repo_path:Function]
# [DEF:init_repo:Function]
@@ -49,6 +60,8 @@ class GitService:
# @PARAM: dashboard_id (int)
# @PARAM: remote_url (str)
# @PARAM: pat (str) - Personal Access Token for authentication.
# @PRE: dashboard_id is int, remote_url is valid Git URL, pat is provided.
# @POST: Repository is cloned or opened at the local path.
# @RETURN: Repo - GitPython Repo object.
def init_repo(self, dashboard_id: int, remote_url: str, pat: str) -> Repo:
with belief_scope("GitService.init_repo"):
@@ -71,7 +84,8 @@ class GitService:
# [DEF:get_repo:Function]
# @PURPOSE: Get Repo object for a dashboard.
# @PRE: Repository must exist on disk.
# @PRE: Repository must exist on disk for the given dashboard_id.
# @POST: Returns a GitPython Repo instance for the dashboard.
# @RETURN: Repo
def get_repo(self, dashboard_id: int) -> Repo:
with belief_scope("GitService.get_repo"):
@@ -88,6 +102,8 @@ class GitService:
# [DEF:list_branches:Function]
# @PURPOSE: List all branches for a dashboard's repository.
# @PRE: Repository for dashboard_id exists.
# @POST: Returns a list of branch metadata dictionaries.
# @RETURN: List[dict]
def list_branches(self, dashboard_id: int) -> List[dict]:
with belief_scope("GitService.list_branches"):
@@ -142,6 +158,8 @@ class GitService:
# @PURPOSE: Create a new branch from an existing one.
# @PARAM: name (str) - New branch name.
# @PARAM: from_branch (str) - Source branch.
# @PRE: Repository exists; name is valid; from_branch exists or repo is empty.
# @POST: A new branch is created in the repository.
def create_branch(self, dashboard_id: int, name: str, from_branch: str = "main"):
with belief_scope("GitService.create_branch"):
repo = self.get_repo(dashboard_id)
@@ -171,10 +189,11 @@ class GitService:
logger.error(f"[create_branch][Coherence:Failed] {e}")
raise
# [/DEF:create_branch:Function]
# [/DEF:create_branch:Function]
# [DEF:checkout_branch:Function]
# @PURPOSE: Switch to a specific branch.
# @PRE: Repository exists and the specified branch name exists.
# @POST: The repository working directory is updated to the specified branch.
def checkout_branch(self, dashboard_id: int, name: str):
with belief_scope("GitService.checkout_branch"):
repo = self.get_repo(dashboard_id)
@@ -186,6 +205,8 @@ class GitService:
# @PURPOSE: Stage and commit changes.
# @PARAM: message (str) - Commit message.
# @PARAM: files (List[str]) - Optional list of specific files to stage.
# @PRE: Repository exists and has changes (dirty) or files are specified.
# @POST: Changes are staged and a new commit is created.
def commit_changes(self, dashboard_id: int, message: str, files: List[str] = None):
with belief_scope("GitService.commit_changes"):
repo = self.get_repo(dashboard_id)
@@ -208,6 +229,8 @@ class GitService:
# [DEF:push_changes:Function]
# @PURPOSE: Push local commits to remote.
# @PRE: Repository exists and has an 'origin' remote.
# @POST: Local branch commits are pushed to origin.
def push_changes(self, dashboard_id: int):
with belief_scope("GitService.push_changes"):
repo = self.get_repo(dashboard_id)
@@ -240,6 +263,8 @@ class GitService:
# [DEF:pull_changes:Function]
# @PURPOSE: Pull changes from remote.
# @PRE: Repository exists and has an 'origin' remote.
# @POST: Changes from origin are pulled and merged into the active branch.
def pull_changes(self, dashboard_id: int):
with belief_scope("GitService.pull_changes"):
repo = self.get_repo(dashboard_id)
@@ -261,6 +286,8 @@ class GitService:
# [DEF:get_status:Function]
# @PURPOSE: Get current repository status (dirty files, untracked, etc.)
# @PRE: Repository for dashboard_id exists.
# @POST: Returns a dictionary representing the Git status.
# @RETURN: dict
def get_status(self, dashboard_id: int) -> dict:
with belief_scope("GitService.get_status"):
@@ -287,6 +314,8 @@ class GitService:
# @PURPOSE: Generate diff for a file or the whole repository.
# @PARAM: file_path (str) - Optional specific file.
# @PARAM: staged (bool) - Whether to show staged changes.
# @PRE: Repository for dashboard_id exists.
# @POST: Returns the diff text as a string.
# @RETURN: str
def get_diff(self, dashboard_id: int, file_path: str = None, staged: bool = False) -> str:
with belief_scope("GitService.get_diff"):
@@ -303,6 +332,8 @@ class GitService:
# [DEF:get_commit_history:Function]
# @PURPOSE: Retrieve commit history for a repository.
# @PARAM: limit (int) - Max number of commits to return.
# @PRE: Repository for dashboard_id exists.
# @POST: Returns a list of dictionaries for each commit in history.
# @RETURN: List[dict]
def get_commit_history(self, dashboard_id: int, limit: int = 50) -> List[dict]:
with belief_scope("GitService.get_commit_history"):
@@ -333,6 +364,8 @@ class GitService:
# @PARAM: provider (GitProvider)
# @PARAM: url (str)
# @PARAM: pat (str)
# @PRE: provider is valid; url is a valid HTTP(S) URL; pat is provided.
# @POST: Returns True if connection to the provider's API succeeds.
# @RETURN: bool
async def test_connection(self, provider: GitProvider, url: str, pat: str) -> bool:
with belief_scope("GitService.test_connection"):

Binary file not shown.

View File

@@ -18,6 +18,4 @@ def test_environment_model():
assert env.id == "test-id"
assert env.name == "test-env"
assert env.url == "http://localhost:8088/api/v1"
# [/DEF:test_superset_config_url_normalization:Function]
# [/DEF:test_superset_config_invalid_url:Function]
# [/DEF:test_environment_model:Function]

View File

@@ -13,7 +13,7 @@ The settings mechanism allows users to configure multiple Superset environments
Configuration is structured using Pydantic models in `backend/src/core/config_models.py`:
- `Environment`: Represents a Superset instance (URL, credentials). The `base_url` is automatically normalized to include the `/api/v1` suffix if missing.
- `GlobalSettings`: Global application parameters (e.g., `backup_path`).
- `GlobalSettings`: Global application parameters (e.g., `storage.root_path`).
- `AppConfig`: The root configuration object.
### Configuration Manager
@@ -43,4 +43,4 @@ The settings page is located at `frontend/src/pages/Settings.svelte`. It provide
Existing plugins and utilities use the `ConfigManager` to fetch configuration:
- `superset_tool/utils/init_clients.py`: Dynamically initializes Superset clients from the configured environments.
- `BackupPlugin`: Uses the configured `backup_path` as the default storage location.
- `BackupPlugin`: Uses the configured `storage.root_path` as the default storage location.

26
frontend/.gitignore vendored Executable file
View File

@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.svelte-kit
build
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -12,6 +12,8 @@
// [SECTION: IMPORTS]
import { createEventDispatcher } from 'svelte';
import type { DashboardMetadata } from '../types/dashboard';
import { t } from '../lib/i18n';
import { Button, Input } from '../lib/ui';
import GitManager from './git/GitManager.svelte';
// [/SECTION]
@@ -143,64 +145,66 @@
<!-- [SECTION: TEMPLATE] -->
<div class="dashboard-grid">
<!-- Filter Input -->
<div class="mb-4">
<input
type="text"
<div class="mb-6">
<Input
bind:value={filterText}
placeholder="Search dashboards..."
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={$t.dashboard.search}
/>
</div>
<!-- Grid/Table -->
<div class="overflow-x-auto">
<table class="min-w-full bg-white border border-gray-300">
<div class="overflow-x-auto rounded-lg border border-gray-200">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 border-b">
<th class="px-6 py-3 text-left">
<input
type="checkbox"
checked={allSelected}
indeterminate={someSelected && !allSelected}
on:change={(e) => handleSelectAll((e.target as HTMLInputElement).checked)}
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</th>
<th class="px-4 py-2 border-b cursor-pointer" on:click={() => handleSort('title')}>
Title {sortColumn === 'title' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors" on:click={() => handleSort('title')}>
{$t.dashboard.title} {sortColumn === 'title' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
</th>
<th class="px-4 py-2 border-b cursor-pointer" on:click={() => handleSort('last_modified')}>
Last Modified {sortColumn === 'last_modified' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors" on:click={() => handleSort('last_modified')}>
{$t.dashboard.last_modified} {sortColumn === 'last_modified' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
</th>
<th class="px-4 py-2 border-b cursor-pointer" on:click={() => handleSort('status')}>
Status {sortColumn === 'status' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors" on:click={() => handleSort('status')}>
{$t.dashboard.status} {sortColumn === 'status' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
</th>
<th class="px-4 py-2 border-b">Git</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.dashboard.git}</th>
</tr>
</thead>
<tbody>
<tbody class="bg-white divide-y divide-gray-200">
{#each paginatedDashboards as dashboard (dashboard.id)}
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 border-b">
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
checked={selectedIds.includes(dashboard.id)}
on:change={(e) => handleSelectionChange(dashboard.id, (e.target as HTMLInputElement).checked)}
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</td>
<td class="px-4 py-2 border-b">{dashboard.title}</td>
<td class="px-4 py-2 border-b">{new Date(dashboard.last_modified).toLocaleDateString()}</td>
<td class="px-4 py-2 border-b">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{dashboard.title}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{new Date(dashboard.last_modified).toLocaleDateString()}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<span class="px-2 py-1 text-xs font-medium rounded-full {dashboard.status === 'published' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}">
{dashboard.status}
</span>
</td>
<td class="px-4 py-2 border-b">
<button
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<Button
variant="ghost"
size="sm"
on:click={() => openGit(dashboard)}
class="text-indigo-600 hover:text-indigo-900 text-sm font-medium"
class="text-blue-600 hover:text-blue-900"
>
Manage Git
</button>
{$t.git.manage}
</Button>
</td>
</tr>
{/each}
@@ -209,25 +213,30 @@
</div>
<!-- Pagination Controls -->
<div class="flex items-center justify-between mt-4">
<div class="text-sm text-gray-700">
Showing {currentPage * pageSize + 1} to {Math.min((currentPage + 1) * pageSize, sortedDashboards.length)} of {sortedDashboards.length} dashboards
<div class="flex items-center justify-between mt-6">
<div class="text-sm text-gray-500">
{($t.dashboard?.showing || "")
.replace('{start}', (currentPage * pageSize + 1).toString())
.replace('{end}', Math.min((currentPage + 1) * pageSize, sortedDashboards.length).toString())
.replace('{total}', sortedDashboards.length.toString())}
</div>
<div class="flex space-x-2">
<button
class="px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
<div class="flex gap-2">
<Button
variant="secondary"
size="sm"
disabled={currentPage === 0}
on:click={() => goToPage(currentPage - 1)}
>
Previous
</button>
<button
class="px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
{$t.dashboard.previous}
</Button>
<Button
variant="secondary"
size="sm"
disabled={currentPage >= totalPages - 1}
on:click={() => goToPage(currentPage + 1)}
>
Next
</button>
{$t.dashboard.next}
</Button>
</div>
</div>
</div>

View File

@@ -57,4 +57,4 @@
/* Component specific styles */
</style>
<!-- [/DEF:EnvSelector:Component] -->
<!-- [/DEF:EnvSelector:Component] -->

View File

@@ -7,61 +7,41 @@
-->
<script>
import { page } from '$app/stores';
import { t } from '$lib/i18n';
import { LanguageSwitcher } from '$lib/ui';
</script>
<header class="bg-white shadow-md p-4 flex justify-between items-center">
<a
href="/"
class="text-3xl font-bold text-gray-800 focus:outline-none"
class="text-2xl font-bold text-gray-800 focus:outline-none"
>
Superset Tools
</a>
<nav class="space-x-4">
<nav class="flex items-center space-x-4">
<a
href="/"
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname === '/' ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
>
Dashboard
</a>
<a
href="/migration"
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname.startsWith('/migration') ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
>
Migration
</a>
<a
href="/git"
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname.startsWith('/git') ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
>
Git
{$t.nav.dashboard}
</a>
<a
href="/tasks"
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname.startsWith('/tasks') ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
>
Tasks
{$t.nav.tasks}
</a>
<div class="relative inline-block group">
<button class="text-gray-600 hover:text-blue-600 font-medium pb-1 {$page.url.pathname.startsWith('/tools') ? 'text-blue-600 border-b-2 border-blue-600' : ''}">
Tools
</button>
<div class="absolute hidden group-hover:block bg-white shadow-lg rounded-md mt-1 py-2 w-48 z-10 border border-gray-100 before:absolute before:-top-2 before:left-0 before:right-0 before:h-2 before:content-[''] right-0">
<a href="/tools/search" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">Dataset Search</a>
<a href="/tools/mapper" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">Dataset Mapper</a>
<a href="/tools/debug" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">System Debug</a>
</div>
</div>
<div class="relative inline-block group">
<button class="text-gray-600 hover:text-blue-600 font-medium pb-1 {$page.url.pathname.startsWith('/settings') ? 'text-blue-600 border-b-2 border-blue-600' : ''}">
Settings
{$t.nav.settings}
</button>
<div class="absolute hidden group-hover:block bg-white shadow-lg rounded-md mt-1 py-2 w-48 z-10 border border-gray-100 before:absolute before:-top-2 before:left-0 before:right-0 before:h-2 before:content-[''] right-0">
<a href="/settings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">General Settings</a>
<a href="/settings/connections" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">Connections</a>
<a href="/settings/git" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">Git Integration</a>
<a href="/settings/environments" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">Environments</a>
<a href="/settings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_general}</a>
<a href="/settings/connections" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_connections}</a>
<a href="/settings/git" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_git}</a>
</div>
</div>
<LanguageSwitcher />
</nav>
</header>
<!-- [/DEF:Navbar:Component] -->

View File

@@ -9,6 +9,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { formatDistanceToNow } from 'date-fns';
import { t } from '../lib/i18n';
export let tasks: Array<any> = [];
export let loading: boolean = false;
@@ -58,9 +59,9 @@
<div class="bg-white shadow overflow-hidden sm:rounded-md">
{#if loading && tasks.length === 0}
<div class="p-4 text-center text-gray-500">Loading tasks...</div>
<div class="p-4 text-center text-gray-500">{$t.tasks?.loading || 'Loading...'}</div>
{:else if tasks.length === 0}
<div class="p-4 text-center text-gray-500">No tasks found.</div>
<div class="p-4 text-center text-gray-500">{$t.tasks?.no_tasks || 'No tasks found.'}</div>
{:else}
<ul class="divide-y divide-gray-200">
{#each tasks as task (task.id)}
@@ -94,7 +95,7 @@
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd" />
</svg>
<p>
Started {formatTime(task.started_at)}
{($t.tasks?.started || "").replace('{time}', formatTime(task.started_at))}
</p>
</div>
</div>

View File

@@ -8,6 +8,8 @@
<script>
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
import { getTaskLogs } from '../services/taskService.js';
import { t } from '../lib/i18n';
import { Button } from '../lib/ui';
export let show = false;
export let inline = false;
@@ -143,20 +145,20 @@
<div class="flex flex-col h-full w-full p-4">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900">
Task Logs <span class="text-sm text-gray-500 font-normal">({taskId})</span>
{$t.tasks?.logs_title} <span class="text-sm text-gray-500 font-normal">({taskId})</span>
</h3>
<button on:click={fetchLogs} class="text-sm text-indigo-600 hover:text-indigo-900">Refresh</button>
<Button variant="ghost" size="sm" on:click={fetchLogs} class="text-blue-600">{$t.tasks?.refresh}</Button>
</div>
<div class="flex-1 border rounded-md bg-gray-50 p-4 overflow-y-auto font-mono text-sm"
<div class="flex-1 border rounded-md bg-gray-50 p-4 overflow-y-auto font-mono text-sm"
bind:this={logContainer}
on:scroll={handleScroll}>
{#if loading && logs.length === 0}
<p class="text-gray-500 text-center">Loading logs...</p>
<p class="text-gray-500 text-center">{$t.tasks?.loading}</p>
{:else if error}
<p class="text-red-500 text-center">{error}</p>
{:else if logs.length === 0}
<p class="text-gray-500 text-center">No logs available.</p>
<p class="text-gray-500 text-center">{$t.tasks?.no_logs}</p>
{:else}
{#each logs as log}
<div class="mb-1 hover:bg-gray-100 p-1 rounded">
@@ -192,19 +194,19 @@
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<h3 class="text-lg leading-6 font-medium text-gray-900 flex justify-between items-center" id="modal-title">
<span>Task Logs <span class="text-sm text-gray-500 font-normal">({taskId})</span></span>
<button on:click={fetchLogs} class="text-sm text-indigo-600 hover:text-indigo-900">Refresh</button>
<span>{$t.tasks.logs_title} <span class="text-sm text-gray-500 font-normal">({taskId})</span></span>
<Button variant="ghost" size="sm" on:click={fetchLogs} class="text-blue-600">{$t.tasks.refresh}</Button>
</h3>
<div class="mt-4 border rounded-md bg-gray-50 p-4 h-96 overflow-y-auto font-mono text-sm"
<div class="mt-4 border rounded-md bg-gray-50 p-4 h-96 overflow-y-auto font-mono text-sm"
bind:this={logContainer}
on:scroll={handleScroll}>
{#if loading && logs.length === 0}
<p class="text-gray-500 text-center">Loading logs...</p>
<p class="text-gray-500 text-center">{$t.tasks.loading}</p>
{:else if error}
<p class="text-red-500 text-center">{error}</p>
{:else if logs.length === 0}
<p class="text-gray-500 text-center">No logs available.</p>
<p class="text-gray-500 text-center">{$t.tasks.no_logs}</p>
{:else}
{#each logs as log}
<div class="mb-1 hover:bg-gray-100 p-1 rounded">
@@ -230,13 +232,9 @@
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
on:click={close}
>
Close
</button>
<Button variant="secondary" on:click={close}>
{$t.common.cancel}
</Button>
</div>
</div>
</div>

View File

@@ -0,0 +1,84 @@
<!-- [DEF:BackupList:Component] -->
<!--
@SEMANTICS: backup, list, table
@PURPOSE: Displays a list of existing backups.
@LAYER: Component
@RELATION: USED_BY -> frontend/src/components/backups/BackupManager.svelte
-->
<script lang="ts">
// [SECTION: IMPORTS]
import { t } from '../../lib/i18n';
import { Button } from '../../lib/ui';
import type { Backup } from '../../types/backup';
import { goto } from '$app/navigation';
// [/SECTION]
// [SECTION: PROPS]
/**
* @type {Backup[]}
* @description Array of backup objects to display.
*/
export let backups: Backup[] = [];
// [/SECTION]
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="overflow-x-auto rounded-lg border border-gray-200">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$t.storage.table.name}
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$t.tasks.target_env}
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$t.storage.table.created_at}
</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
{$t.storage.table.actions}
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each backups as backup}
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{backup.name}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{backup.environment}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(backup.created_at).toLocaleString()}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Button
variant="ghost"
size="sm"
class="text-blue-600 hover:text-blue-900"
on:click={() => goto(`/tools/storage?path=backups/${backup.name}`)}
>
{$t.storage.table.go_to_storage}
</Button>
</td>
</tr>
{:else}
<tr>
<td colspan="4" class="px-6 py-10 text-center text-gray-500">
{$t.storage.no_files}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- [/SECTION] -->
<style>
</style>
<!-- [/DEF:BackupList:Component] -->

View File

@@ -0,0 +1,241 @@
<!-- [DEF:BackupManager:Component] -->
<!--
@SEMANTICS: backup, manager, orchestrator
@PURPOSE: Main container for backup management, handling creation and listing.
@LAYER: Feature
@RELATION: USES -> BackupList
@RELATION: USES -> api
@INVARIANT: Only one backup task can be triggered at a time from the UI.
-->
<script lang="ts">
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { t } from '../../lib/i18n';
import { api, requestApi } from '../../lib/api';
import { addToast } from '../../lib/toasts';
import { Button, Card, Select, Input } from '../../lib/ui';
import BackupList from './BackupList.svelte';
import type { Backup } from '../../types/backup';
// [/SECTION]
// [SECTION: STATE]
let backups: Backup[] = [];
let environments: any[] = [];
let selectedEnvId = '';
let loading = true;
let creating = false;
let savingSchedule = false;
// Schedule state for selected environment
let scheduleEnabled = false;
let cronExpression = '0 0 * * *';
$: selectedEnv = environments.find(e => e.id === selectedEnvId);
$: if (selectedEnv) {
scheduleEnabled = selectedEnv.backup_schedule?.enabled ?? false;
cronExpression = selectedEnv.backup_schedule?.cron_expression ?? '0 0 * * *';
}
// [/SECTION]
// [DEF:loadData:Function]
/**
* @purpose Loads backups and environments from the backend.
*
* @pre API must be reachable.
* @post environments and backups stores are populated.
*
* @returns {Promise<void>}
* @side_effect Updates local state variables.
*/
// @RELATION: CALLS -> api.getEnvironmentsList
// @RELATION: CALLS -> api.requestApi
async function loadData() {
console.log("[BackupManager][Entry] Loading data.");
loading = true;
try {
const [envsData, storageData] = await Promise.all([
api.getEnvironmentsList(),
requestApi('/storage/files?category=backups')
]);
environments = envsData;
// Map storage files to Backup type
backups = (storageData || []).map((file: any) => ({
id: file.name,
name: file.name,
environment: file.path.split('/')[0] || 'Unknown',
created_at: file.created_at,
size_bytes: file.size,
status: 'success'
}));
console.log("[BackupManager][Action] Data loaded successfully.");
} catch (error) {
console.error("[BackupManager][Coherence:Failed] Load failed", error);
} finally {
loading = false;
}
}
// [/DEF:loadData:Function]
// [DEF:handleCreateBackup:Function]
/**
* @purpose Triggers a new backup task for the selected environment.
*
* @pre selectedEnvId must be a valid environment ID.
* @post A new task is created on the backend.
*
* @returns {Promise<void>}
* @side_effect Dispatches a toast notification.
*/
// @RELATION: CALLS -> api.createTask
// [DEF:handleUpdateSchedule:Function]
/**
* @purpose Updates the backup schedule for the selected environment.
* @pre selectedEnvId must be set.
* @post Environment config is updated on the backend.
*/
async function handleUpdateSchedule() {
if (!selectedEnvId) return;
console.log(`[BackupManager][Action] Updating schedule for env: ${selectedEnvId}`);
savingSchedule = true;
try {
await api.updateEnvironmentSchedule(selectedEnvId, {
enabled: scheduleEnabled,
cron_expression: cronExpression
});
addToast($t.common.success, 'success');
// Update local state
environments = environments.map(e =>
e.id === selectedEnvId
? { ...e, backup_schedule: { enabled: scheduleEnabled, cron_expression: cronExpression } }
: e
);
} catch (error) {
console.error("[BackupManager][Coherence:Failed] Schedule update failed", error);
} finally {
savingSchedule = false;
}
}
// [/DEF:handleUpdateSchedule:Function]
async function handleCreateBackup() {
if (!selectedEnvId) {
addToast($t.tasks.select_env, 'error');
return;
}
console.log(`[BackupManager][Action] Triggering backup for env: ${selectedEnvId}`);
creating = true;
try {
await api.createTask('superset-backup', { environment_id: selectedEnvId });
addToast($t.common.success, 'success');
console.log("[BackupManager][Coherence:OK] Backup task triggered.");
} catch (error) {
console.error("[BackupManager][Coherence:Failed] Create failed", error);
} finally {
creating = false;
}
}
// [/DEF:handleCreateBackup:Function]
onMount(loadData);
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="space-y-6">
<Card title={$t.tasks.manual_backup}>
<div class="space-y-4">
<div class="flex items-end gap-4">
<div class="flex-1">
<Select
label={$t.tasks.target_env}
bind:value={selectedEnvId}
options={[
{ value: '', label: $t.tasks.select_env },
...environments.map(e => ({ value: e.id, label: e.name }))
]}
/>
</div>
<Button
variant="primary"
on:click={handleCreateBackup}
disabled={creating || !selectedEnvId}
>
{creating ? $t.common.loading : $t.tasks.start_backup}
</Button>
</div>
{#if selectedEnvId}
<div class="pt-6 border-t border-gray-100 mt-4">
<h3 class="text-sm font-semibold text-gray-800 mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{$t.tasks.backup_schedule}
</h3>
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div class="flex flex-col md:flex-row md:items-start gap-6">
<div class="pt-8">
<label class="flex items-center gap-3 cursor-pointer group">
<div class="relative inline-flex items-center">
<input type="checkbox" bind:checked={scheduleEnabled} class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-gray-900 transition-colors">{$t.tasks.schedule_enabled}</span>
</div>
</label>
</div>
<div class="flex-1 space-y-2">
<Input
label={$t.tasks.cron_label}
placeholder="0 0 * * *"
bind:value={cronExpression}
disabled={!scheduleEnabled}
/>
<p class="text-xs text-gray-500 italic">{$t.tasks.cron_hint}</p>
</div>
<div class="pt-8">
<Button
variant="secondary"
on:click={handleUpdateSchedule}
disabled={savingSchedule}
class="min-w-[100px]"
>
{#if savingSchedule}
<span class="flex items-center gap-2">
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{$t.common.loading}
</span>
{:else}
{$t.common.save}
{/if}
</Button>
</div>
</div>
</div>
</div>
{/if}
</div>
</Card>
<div class="space-y-3">
<h2 class="text-lg font-semibold text-gray-700">{$t.storage.backups}</h2>
{#if loading}
<div class="py-10 text-center text-gray-500">{$t.common.loading}</div>
{:else}
<BackupList {backups} />
{/if}
</div>
</div>
<!-- [/SECTION] -->
<!-- [/DEF:BackupManager:Component] -->

View File

@@ -14,6 +14,8 @@
import { onMount, createEventDispatcher } from 'svelte';
import { gitService } from '../../services/gitService';
import { addToast as toast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Select, Input } from '../../lib/ui';
// [/SECTION]
// [SECTION: PROPS]
@@ -31,6 +33,11 @@
const dispatch = createEventDispatcher();
// [DEF:onMount:Function]
/**
* @purpose Load branches when component is mounted.
* @pre Component is initialized.
* @post loadBranches is called.
*/
onMount(async () => {
await loadBranches();
});
@@ -39,6 +46,7 @@
// [DEF:loadBranches:Function]
/**
* @purpose Загружает список веток для дашборда.
* @pre dashboardId is provided.
* @post branches обновлен.
*/
async function loadBranches() {
@@ -57,6 +65,11 @@
// [/DEF:loadBranches:Function]
// [DEF:handleSelect:Function]
/**
* @purpose Handles branch selection from dropdown.
* @pre event contains branch name.
* @post handleCheckout is called with selected branch.
*/
function handleSelect(event) {
handleCheckout(event.target.value);
}
@@ -86,7 +99,8 @@
// [DEF:handleCreate:Function]
/**
* @purpose Создает новую ветку.
* @post Новая ветка создана и загружена.
* @pre newBranchName is not empty.
* @post Новая ветка создана и загружена; showCreate reset.
*/
async function handleCreate() {
if (!newBranchName) return;
@@ -107,61 +121,55 @@
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<div class="relative">
<select
value={currentBranch}
<div class="space-y-3">
<div class="flex items-center gap-3">
<div class="flex-grow">
<Select
bind:value={currentBranch}
on:change={handleSelect}
disabled={loading}
class="bg-white border rounded px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{#each branches as branch}
<option value={branch.name}>{branch.name}</option>
{/each}
</select>
{#if loading}
<span class="absolute -right-6 top-1">
<svg class="animate-spin h-4 w-4 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
{/if}
options={branches.map(b => ({ value: b.name, label: b.name }))}
/>
</div>
<button
<Button
variant="ghost"
size="sm"
on:click={() => showCreate = !showCreate}
disabled={loading}
class="text-blue-600 hover:text-blue-800 text-sm font-medium disabled:opacity-50"
class="text-blue-600"
>
+ New Branch
</button>
+ {$t.git.new_branch}
</Button>
</div>
{#if showCreate}
<div class="flex items-center space-x-1 bg-gray-50 p-2 rounded border border-dashed">
<input
type="text"
bind:value={newBranchName}
placeholder="branch-name"
disabled={loading}
class="border rounded px-2 py-1 text-sm w-full max-w-[150px]"
/>
<button
<div class="flex items-end gap-2 bg-gray-50 p-3 rounded-lg border border-dashed border-gray-200">
<div class="flex-grow">
<Input
bind:value={newBranchName}
placeholder="branch-name"
disabled={loading}
/>
</div>
<Button
variant="primary"
size="sm"
on:click={handleCreate}
disabled={loading || !newBranchName}
class="bg-green-600 text-white px-3 py-1 rounded text-xs font-medium hover:bg-green-700 disabled:opacity-50"
isLoading={loading}
class="bg-green-600 hover:bg-green-700"
>
{loading ? '...' : 'Create'}
</button>
<button
{$t.git.create}
</Button>
<Button
variant="ghost"
size="sm"
on:click={() => showCreate = false}
disabled={loading}
class="text-gray-500 hover:text-gray-700 text-xs px-2 py-1 disabled:opacity-50"
>
Cancel
</button>
{$t.common.cancel}
</Button>
</div>
{/if}
</div>

View File

@@ -11,6 +11,8 @@
import { onMount } from 'svelte';
import { gitService } from '../../services/gitService';
import { addToast as toast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button } from '../../lib/ui';
// [/SECTION]
// [SECTION: PROPS]
@@ -25,6 +27,8 @@
// [DEF:onMount:Function]
/**
* @purpose Load history when component is mounted.
* @pre Component is initialized with dashboardId.
* @post loadHistory is called.
*/
onMount(async () => {
await loadHistory();
@@ -34,6 +38,7 @@
// [DEF:loadHistory:Function]
/**
* @purpose Fetch commit history from the backend.
* @pre dashboardId is valid.
* @post history state is updated.
*/
async function loadHistory() {
@@ -53,22 +58,22 @@
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="mt-6">
<h3 class="text-lg font-semibold mb-4 flex justify-between items-center">
Commit History
<button on:click={loadHistory} class="text-sm text-blue-600 hover:underline">Refresh</button>
</h3>
<div class="mt-2">
<div class="flex justify-between items-center mb-6">
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider">
{$t.git.history}
</h3>
<Button variant="ghost" size="sm" on:click={loadHistory} class="text-blue-600">
{$t.git.refresh}
</Button>
</div>
{#if loading}
<div class="flex items-center space-x-2 text-gray-500">
<svg class="animate-spin h-4 w-4 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Loading history...</span>
<div class="flex items-center justify-center py-12">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
</div>
{:else if history.length === 0}
<p class="text-gray-500 italic">No commits yet.</p>
<p class="text-gray-500 italic text-center py-12">{$t.git.no_commits}</p>
{:else}
<div class="space-y-3 max-h-96 overflow-y-auto pr-2">
{#each history as commit}

View File

@@ -32,6 +32,7 @@
// [DEF:loadStatus:Watcher]
$: if (show) loadEnvironments();
// [/DEF:loadStatus:Watcher]
// [DEF:loadEnvironments:Function]
/**

View File

@@ -16,6 +16,8 @@
import { onMount } from 'svelte';
import { gitService } from '../../services/gitService';
import { addToast as toast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Card, PageHeader, Select, Input } from '../../lib/ui';
import BranchSelector from './BranchSelector.svelte';
import CommitModal from './CommitModal.svelte';
import CommitHistory from './CommitHistory.svelte';
@@ -49,6 +51,8 @@
// [DEF:checkStatus:Function]
/**
* @purpose Проверяет, инициализирован ли репозиторий для данного дашборда.
* @pre Component is mounted and has dashboardId.
* @post initialized state is set; configs loaded if not initialized.
*/
async function checkStatus() {
checkingStatus = true;
@@ -70,6 +74,8 @@
// [DEF:handleInit:Function]
/**
* @purpose Инициализирует репозиторий для дашборда.
* @pre selectedConfigId and remoteUrl are provided.
* @post Repository is created on backend; initialized set to true.
*/
async function handleInit() {
if (!selectedConfigId || !remoteUrl) {
@@ -92,6 +98,8 @@
// [DEF:handleSync:Function]
/**
* @purpose Синхронизирует состояние Superset с локальным Git-репозиторием.
* @pre Repository is initialized.
* @post Dashboard YAMLs are exported to Git and staged.
*/
async function handleSync() {
loading = true;
@@ -109,6 +117,11 @@
// [/DEF:handleSync:Function]
// [DEF:handlePush:Function]
/**
* @purpose Pushes local commits to the remote repository.
* @pre Repository is initialized and has commits.
* @post Changes are pushed to origin.
*/
async function handlePush() {
loading = true;
try {
@@ -123,6 +136,11 @@
// [/DEF:handlePush:Function]
// [DEF:handlePull:Function]
/**
* @purpose Pulls changes from the remote repository.
* @pre Repository is initialized.
* @post Local branch is updated with remote changes.
*/
async function handlePull() {
loading = true;
try {
@@ -143,17 +161,16 @@
{#if show}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded-lg shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<div class="flex justify-between items-center mb-6 border-b pb-4">
<div>
<h2 class="text-2xl font-bold">Git Management: {dashboardTitle}</h2>
<p class="text-sm text-gray-500">ID: {dashboardId}</p>
<PageHeader title="{$t.git.management}: {dashboardTitle}">
<div slot="subtitle" class="text-sm text-gray-500">ID: {dashboardId}</div>
<div slot="actions">
<button on:click={() => show = false} class="text-gray-400 hover:text-gray-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<button on:click={() => show = false} class="text-gray-500 hover:text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</PageHeader>
{#if checkingStatus}
<div class="flex justify-center py-12">
@@ -161,95 +178,94 @@
</div>
{:else if !initialized}
<div class="max-w-md mx-auto py-8">
<div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-6">
<p class="text-sm text-blue-700">
This dashboard is not yet linked to a Git repository.
Please configure the repository details below.
<Card>
<p class="text-sm text-gray-600 mb-6">
{$t.git.not_linked}
</p>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Git Server</label>
<select bind:value={selectedConfigId} class="mt-1 block w-full border rounded p-2">
{#each configs as config}
<option value={config.id}>{config.name} ({config.provider})</option>
{/each}
</select>
{#if configs.length === 0}
<p class="text-xs text-red-500 mt-1">No Git servers configured. Go to Settings -> Git to add one.</p>
{/if}
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Remote Repository URL</label>
<input
type="text"
bind:value={remoteUrl}
placeholder="https://github.com/org/repo.git"
class="mt-1 block w-full border rounded p-2"
<div class="space-y-6">
<Select
label={$t.git.server}
bind:value={selectedConfigId}
options={configs.map(c => ({ value: c.id, label: `${c.name} (${c.provider})` }))}
/>
{#if configs.length === 0}
<p class="text-xs text-red-500 -mt-4">No Git servers configured. Go to Settings -> Git to add one.</p>
{/if}
<Input
label={$t.git.remote_url}
bind:value={remoteUrl}
placeholder="https://github.com/org/repo.git"
/>
<Button
on:click={handleInit}
disabled={loading || configs.length === 0}
isLoading={loading}
class="w-full"
>
{$t.git.init_repo}
</Button>
</div>
<button
on:click={handleInit}
disabled={loading || configs.length === 0}
class="w-full bg-blue-600 text-white py-2 rounded font-medium hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Initializing...' : 'Initialize Repository'}
</button>
</div>
</Card>
</div>
{:else}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Left Column: Controls -->
<div class="md:col-span-1 space-y-6">
<section>
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-2">Branch</h3>
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">{$t.git.branch}</h3>
<BranchSelector {dashboardId} bind:currentBranch />
</section>
<section class="space-y-2">
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-2">Actions</h3>
<button
<section class="space-y-3">
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">{$t.git.actions}</h3>
<Button
variant="secondary"
on:click={handleSync}
disabled={loading}
class="w-full flex items-center justify-center px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded text-sm font-medium transition"
class="w-full"
>
Sync from Superset
</button>
<button
{$t.git.sync}
</Button>
<Button
on:click={() => showCommitModal = true}
disabled={loading}
class="w-full flex items-center justify-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm font-medium transition"
class="w-full"
>
Commit Changes
</button>
<div class="grid grid-cols-2 gap-2">
<button
{$t.git.commit}
</Button>
<div class="grid grid-cols-2 gap-3">
<Button
variant="ghost"
on:click={handlePull}
disabled={loading}
class="flex items-center justify-center px-4 py-2 border hover:bg-gray-50 rounded text-sm font-medium transition"
class="border border-gray-200"
>
Pull
</button>
<button
{$t.git.pull}
</Button>
<Button
variant="ghost"
on:click={handlePush}
disabled={loading}
class="flex items-center justify-center px-4 py-2 border hover:bg-gray-50 rounded text-sm font-medium transition"
class="border border-gray-200"
>
Push
</button>
{$t.git.push}
</Button>
</div>
</section>
<section>
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-2">Deployment</h3>
<button
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">{$t.git.deployment}</h3>
<Button
variant="primary"
on:click={() => showDeployModal = true}
disabled={loading}
class="w-full flex items-center justify-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded text-sm font-medium transition"
class="w-full bg-green-600 hover:bg-green-700 focus-visible:ring-green-500"
>
Deploy to Environment
</button>
{$t.git.deploy}
</Button>
</section>
</div>

View File

@@ -0,0 +1,134 @@
<!-- [DEF:FileList:Component] -->
<!--
@SEMANTICS: storage, files, list, table
@PURPOSE: Displays a table of files with metadata and actions.
@LAYER: Component
@RELATION: DEPENDS_ON -> storageService
@PROPS: files (Array) - List of StoredFile objects.
@EVENTS: delete (filename) - Dispatched when a file is deleted.
-->
<script lang="ts">
// [SECTION: IMPORTS]
import { createEventDispatcher } from 'svelte';
import { downloadFileUrl } from '../../services/storageService';
import { t } from '../../lib/i18n';
// [/SECTION: IMPORTS]
export let files = [];
const dispatch = createEventDispatcher();
// [DEF:isDirectory:Function]
/**
* @purpose Checks if a file object represents a directory.
* @param {Object} file - The file object to check.
* @return {boolean} True if it's a directory, false otherwise.
*/
function isDirectory(file) {
return file.mime_type === 'directory';
}
// [/DEF:isDirectory:Function]
// [DEF:formatSize:Function]
/**
* @purpose Formats file size in bytes into a human-readable string.
* @param {number} bytes - The size in bytes.
* @return {string} Formatted size (e.g., "1.2 MB").
*/
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// [/DEF:formatSize:Function]
// [DEF:formatDate:Function]
/**
* @purpose Formats an ISO date string into a localized readable format.
* @param {string} dateStr - The date string to format.
* @return {string} Localized date and time.
*/
function formatDate(dateStr) {
return new Date(dateStr).toLocaleString();
}
// [/DEF:formatDate:Function]
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="overflow-x-auto">
<table class="min-w-full bg-white border border-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.storage.table.name}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.storage.table.category}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.storage.table.size}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.storage.table.created_at}</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.storage.table.actions}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each files as file}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{#if isDirectory(file)}
<button
on:click={() => dispatch('navigate', file.path)}
class="flex items-center text-indigo-600 hover:text-indigo-900"
>
<svg class="h-5 w-5 mr-2 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
</svg>
{file.name}
</button>
{:else}
<div class="flex items-center">
<svg class="h-5 w-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
{file.name}
</div>
{/if}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 capitalize">{file.category}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{isDirectory(file) ? '--' : formatSize(file.size)}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{formatDate(file.created_at)}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
{#if !isDirectory(file)}
<a
href={downloadFileUrl(file.category, file.path)}
download={file.name}
class="text-indigo-600 hover:text-indigo-900 mr-4"
>
{$t.storage.table.download}
</a>
{/if}
<button
on:click={() => dispatch('delete', { category: file.category, path: file.path, name: file.name })}
class="text-red-600 hover:text-red-900"
>
{$t.storage.table.delete}
</button>
</td>
</tr>
{:else}
<tr>
<td colspan="5" class="px-6 py-10 text-center text-sm text-gray-500">
{$t.storage.no_files}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- [/SECTION: TEMPLATE] -->
<style>
/* ... */
</style>
<!-- [/DEF:FileList:Component] -->

View File

@@ -0,0 +1,134 @@
<!-- [DEF:FileUpload:Component] -->
<!--
@SEMANTICS: storage, upload, files
@PURPOSE: Provides a form for uploading files to a specific category.
@LAYER: Component
@RELATION: DEPENDS_ON -> storageService
@PROPS: None
@EVENTS: uploaded - Dispatched when a file is successfully uploaded.
-->
<script lang="ts">
// [SECTION: IMPORTS]
import { createEventDispatcher } from 'svelte';
import { uploadFile } from '../../services/storageService';
import { addToast } from '../../lib/toasts';
import { t } from '../../lib/i18n';
// [/SECTION: IMPORTS]
// [DEF:handleUpload:Function]
/**
* @purpose Handles the file upload process.
* @pre A file must be selected in the file input.
* @post The file is uploaded to the server and a success toast is shown.
*/
const dispatch = createEventDispatcher();
let fileInput;
export let category = 'backups';
export let path = '';
let isUploading = false;
let dragOver = false;
async function handleUpload() {
const file = fileInput.files[0];
if (!file) return;
isUploading = true;
try {
// path is relative to root, but upload endpoint expects path within category
// FileList.path is like "backup/folder", we need just "folder"
const subpath = path.startsWith(category)
? path.substring(category.length).replace(/^\/+/, '')
: path;
await uploadFile(file, category, subpath);
addToast($t.storage.messages.upload_success.replace('{name}', file.name), 'success');
fileInput.value = '';
dispatch('uploaded');
} catch (error) {
addToast($t.storage.messages.upload_failed.replace('{error}', error.message), 'error');
} finally {
isUploading = false;
}
}
// [/DEF:handleUpload:Function]
// [DEF:handleDrop:Function]
/**
* @purpose Handles the file drop event for drag-and-drop.
* @param {DragEvent} event - The drop event.
*/
function handleDrop(event) {
event.preventDefault();
dragOver = false;
const files = event.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
handleUpload();
}
}
// [/DEF:handleDrop:Function]
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
<h2 class="text-lg font-semibold mb-4">{$t.storage.upload_title}</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{$t.storage.target_category}</label>
<select
bind:value={category}
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option value="backups">{$t.storage.backups}</option>
<option value="repositorys">{$t.storage.repositories}</option>
</select>
</div>
<div
class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-dashed rounded-md transition-colors
{dragOver ? 'border-indigo-500 bg-indigo-50' : 'border-gray-300'}"
on:dragover|preventDefault={() => dragOver = true}
on:dragleave|preventDefault={() => dragOver = false}
on:drop|preventDefault={handleDrop}
>
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
<span>{$t.storage.upload_button}</span>
<input
id="file-upload"
name="file-upload"
type="file"
class="sr-only"
bind:this={fileInput}
on:change={handleUpload}
disabled={isUploading}
>
</label>
<p class="pl-1">{$t.storage.drag_drop}</p>
</div>
<p class="text-xs text-gray-500">{$t.storage.supported_formats}</p>
</div>
</div>
{#if isUploading}
<div class="flex items-center justify-center space-x-2 text-indigo-600">
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-indigo-600"></div>
<span class="text-sm font-medium">{$t.storage.uploading}</span>
</div>
{/if}
</div>
</div>
<!-- [/SECTION: TEMPLATE] -->
<style>
/* ... */
</style>
<!-- [/DEF:FileUpload:Component] -->

View File

@@ -10,6 +10,8 @@
import { createEventDispatcher } from 'svelte';
import { createConnection } from '../../services/connectionService.js';
import { addToast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Input, Card } from '../../lib/ui';
// [/SECTION]
const dispatch = createEventDispatcher();
@@ -17,7 +19,7 @@
let name = '';
let type = 'postgres';
let host = '';
let port = 5432;
let port = "5432";
let database = '';
let username = '';
let password = '';
@@ -36,7 +38,7 @@
isSubmitting = true;
try {
const newConnection = await createConnection({
name, type, host, port, database, username, password
name, type, host, port: Number(port), database, username, password
});
addToast('Connection created successfully', 'success');
dispatch('success', newConnection);
@@ -57,7 +59,7 @@
function resetForm() {
name = '';
host = '';
port = 5432;
port = "5432";
database = '';
username = '';
password = '';
@@ -66,43 +68,28 @@
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 class="text-lg font-medium text-gray-900 mb-4">Add New Connection</h3>
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
<div>
<label for="conn-name" class="block text-sm font-medium text-gray-700">Connection Name</label>
<input type="text" id="conn-name" bind:value={name} placeholder="e.g. Production DWH" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
<Card title={$t.connections?.add_new || "Add New Connection"}>
<form on:submit|preventDefault={handleSubmit} class="space-y-6">
<Input label={$t.connections?.name || "Connection Name"} bind:value={name} placeholder="e.g. Production DWH" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<Input label={$t.connections?.host || "Host"} bind:value={host} placeholder="10.0.0.1" />
<Input label={$t.connections?.port || "Port"} type="number" bind:value={port} />
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="conn-host" class="block text-sm font-medium text-gray-700">Host</label>
<input type="text" id="conn-host" bind:value={host} placeholder="10.0.0.1" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div>
<label for="conn-port" class="block text-sm font-medium text-gray-700">Port</label>
<input type="number" id="conn-port" bind:value={port} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
</div>
<div>
<label for="conn-db" class="block text-sm font-medium text-gray-700">Database Name</label>
<input type="text" id="conn-db" bind:value={database} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="conn-user" class="block text-sm font-medium text-gray-700">Username</label>
<input type="text" id="conn-user" bind:value={username} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div>
<label for="conn-pass" class="block text-sm font-medium text-gray-700">Password</label>
<input type="password" id="conn-pass" bind:value={password} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<Input label={$t.connections?.db_name || "Database Name"} bind:value={database} />
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<Input label={$t.connections?.user || "Username"} bind:value={username} />
<Input label={$t.connections?.pass || "Password"} type="password" bind:value={password} />
</div>
<div class="flex justify-end pt-2">
<button type="submit" disabled={isSubmitting} class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50">
{isSubmitting ? 'Creating...' : 'Create Connection'}
</button>
<Button type="submit" disabled={isSubmitting} isLoading={isSubmitting}>
{$t.connections?.create || "Create Connection"}
</Button>
</div>
</form>
</div>
</Card>
<!-- [/SECTION] -->
<!-- [/DEF:ConnectionForm:Component] -->

View File

@@ -10,6 +10,8 @@
import { onMount, createEventDispatcher } from 'svelte';
import { getConnections, deleteConnection } from '../../services/connectionService.js';
import { addToast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Card } from '../../lib/ui';
// [/SECTION]
const dispatch = createEventDispatcher();
@@ -57,32 +59,30 @@
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="bg-white shadow overflow-hidden sm:rounded-md border border-gray-200">
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">Saved Connections</h3>
</div>
<ul class="divide-y divide-gray-200">
<Card title={$t.connections?.saved || "Saved Connections"} padding="none">
<ul class="divide-y divide-gray-100">
{#if isLoading}
<li class="p-4 text-center text-gray-500">Loading...</li>
<li class="p-6 text-center text-gray-500">{$t.common.loading}</li>
{:else if connections.length === 0}
<li class="p-8 text-center text-gray-500 italic">No connections saved yet.</li>
<li class="p-12 text-center text-gray-500 italic">{$t.connections?.no_saved || "No connections saved yet."}</li>
{:else}
{#each connections as conn}
<li class="p-4 flex items-center justify-between hover:bg-gray-50">
<li class="p-6 flex items-center justify-between hover:bg-gray-50 transition-colors">
<div>
<div class="text-sm font-medium text-indigo-600 truncate">{conn.name}</div>
<div class="text-xs text-gray-500">{conn.type}://{conn.username}@{conn.host}:{conn.port}/{conn.database}</div>
<div class="text-sm font-medium text-blue-600 truncate">{conn.name}</div>
<div class="text-xs text-gray-400 mt-1 font-mono">{conn.type}://{conn.username}@{conn.host}:{conn.port}/{conn.database}</div>
</div>
<button
<Button
variant="danger"
size="sm"
on:click={() => handleDelete(conn.id)}
class="ml-2 inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Delete
</button>
{$t.connections?.delete || "Delete"}
</Button>
</li>
{/each}
{/if}
</ul>
</div>
</Card>
<!-- [/SECTION] -->
<!-- [/DEF:ConnectionList:Component] -->

View File

@@ -13,6 +13,8 @@
import { getConnections } from '../../services/connectionService.js';
import { selectedTask } from '../../lib/stores.js';
import { addToast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Card, Select, Input } from '../../lib/ui';
// [/SECTION]
let envs = [];
@@ -36,7 +38,7 @@
envs = await envsRes.json();
connections = await getConnections();
} catch (e) {
addToast('Failed to fetch data', 'error');
addToast($t.mapper.errors.fetch_failed, 'error');
}
}
// [/DEF:fetchData:Function]
@@ -47,17 +49,17 @@
// @POST: Mapper task is started and selectedTask is updated.
async function handleRunMapper() {
if (!selectedEnv || !datasetId) {
addToast('Please fill in required fields', 'warning');
addToast($t.mapper.errors.required_fields, 'warning');
return;
}
if (source === 'postgres' && (!selectedConnection || !tableName)) {
addToast('Connection and Table Name are required for postgres source', 'warning');
addToast($t.mapper.errors.postgres_required, 'warning');
return;
}
if (source === 'excel' && !excelPath) {
addToast('Excel path is required for excel source', 'warning');
addToast($t.mapper.errors.excel_required, 'warning');
return;
}
@@ -75,7 +77,7 @@
});
selectedTask.set(task);
addToast('Mapper task started', 'success');
addToast($t.mapper.success.started, 'success');
} catch (e) {
addToast(e.message, 'error');
} finally {
@@ -88,78 +90,94 @@
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 class="text-lg font-medium text-gray-900 mb-4">Dataset Column Mapper</h3>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="mapper-env" class="block text-sm font-medium text-gray-700">Environment</label>
<select id="mapper-env" bind:value={selectedEnv} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="" disabled>-- Select Environment --</option>
{#each envs as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
</div>
<div>
<label for="mapper-ds-id" class="block text-sm font-medium text-gray-700">Dataset ID</label>
<input type="number" id="mapper-ds-id" bind:value={datasetId} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Mapping Source</label>
<div class="mt-2 flex space-x-4">
<label class="inline-flex items-center">
<input type="radio" bind:group={source} value="postgres" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300" />
<span class="ml-2 text-sm text-gray-700">PostgreSQL</span>
</label>
<label class="inline-flex items-center">
<input type="radio" bind:group={source} value="excel" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300" />
<span class="ml-2 text-sm text-gray-700">Excel</span>
</label>
</div>
</div>
{#if source === 'postgres'}
<div class="space-y-4 p-4 bg-gray-50 rounded-md border border-gray-100">
<div class="space-y-6">
<Card title={$t.mapper.title}>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="mapper-conn" class="block text-sm font-medium text-gray-700">Saved Connection</label>
<select id="mapper-conn" bind:value={selectedConnection} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="" disabled>-- Select Connection --</option>
{#each connections as conn}
<option value={conn.id}>{conn.name}</option>
{/each}
</select>
<Select
label={$t.mapper.environment}
bind:value={selectedEnv}
options={[
{ value: '', label: $t.mapper.select_env },
...envs.map(e => ({ value: e.id, label: e.name }))
]}
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="mapper-table" class="block text-sm font-medium text-gray-700">Table Name</label>
<input type="text" id="mapper-table" bind:value={tableName} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div>
<label for="mapper-schema" class="block text-sm font-medium text-gray-700">Table Schema</label>
<input type="text" id="mapper-schema" bind:value={tableSchema} class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div>
<Input
label={$t.mapper.dataset_id}
type="number"
bind:value={datasetId}
/>
</div>
</div>
{:else}
<div class="p-4 bg-gray-50 rounded-md border border-gray-100">
<label for="mapper-excel" class="block text-sm font-medium text-gray-700">Excel File Path</label>
<input type="text" id="mapper-excel" bind:value={excelPath} placeholder="/path/to/mapping.xlsx" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
{/if}
<div class="flex justify-end">
<button
on:click={handleRunMapper}
disabled={isRunning}
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{isRunning ? 'Starting...' : 'Run Mapper'}
</button>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{$t.mapper.source}</label>
<div class="flex space-x-4">
<label class="inline-flex items-center">
<input type="radio" bind:group={source} value="postgres" class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" />
<span class="ml-2 text-sm text-gray-700">{$t.mapper.source_postgres}</span>
</label>
<label class="inline-flex items-center">
<input type="radio" bind:group={source} value="excel" class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" />
<span class="ml-2 text-sm text-gray-700">{$t.mapper.source_excel}</span>
</label>
</div>
</div>
{#if source === 'postgres'}
<div class="space-y-4 p-4 bg-gray-50 rounded-md border border-gray-100">
<div>
<Select
label={$t.mapper.connection}
bind:value={selectedConnection}
options={[
{ value: '', label: $t.mapper.select_connection },
...connections.map(c => ({ value: c.id, label: c.name }))
]}
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Input
label={$t.mapper.table_name}
type="text"
bind:value={tableName}
/>
</div>
<div>
<Input
label={$t.mapper.table_schema}
type="text"
bind:value={tableSchema}
/>
</div>
</div>
</div>
{:else}
<div class="p-4 bg-gray-50 rounded-md border border-gray-100">
<Input
label={$t.mapper.excel_path}
type="text"
bind:value={excelPath}
placeholder="/path/to/mapping.xlsx"
/>
</div>
{/if}
<div class="flex justify-end pt-2">
<Button
variant="primary"
on:click={handleRunMapper}
disabled={isRunning}
>
{isRunning ? $t.mapper.starting : $t.mapper.run}
</Button>
</div>
</div>
</div>
</Card>
</div>
<!-- [/SECTION] -->
<!-- [/DEF:MapperTool:Component] -->

View File

@@ -1,186 +0,0 @@
<!-- [DEF:SearchTool:Component] -->
<!--
@SEMANTICS: search, tool, dataset, regex
@PURPOSE: UI component for searching datasets using the SearchPlugin.
@LAYER: UI
@RELATION: USES -> frontend/src/services/toolsService.js
-->
<script>
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { runTask, getTaskStatus } from '../../services/toolsService.js';
import { selectedTask } from '../../lib/stores.js';
import { addToast } from '../../lib/toasts.js';
// [/SECTION]
let envs = [];
let selectedEnv = '';
let searchQuery = '';
let isRunning = false;
let results = null;
let pollInterval;
// [DEF:fetchEnvironments:Function]
// @PURPOSE: Fetches the list of available environments.
// @PRE: None.
// @POST: envs array is populated.
async function fetchEnvironments() {
try {
const res = await fetch('/api/environments');
envs = await res.json();
} catch (e) {
addToast('Failed to fetch environments', 'error');
}
}
// [/DEF:fetchEnvironments:Function]
// [DEF:handleSearch:Function]
// @PURPOSE: Triggers the SearchPlugin task.
// @PRE: selectedEnv and searchQuery must be set.
// @POST: Task is started and polling begins.
async function handleSearch() {
if (!selectedEnv || !searchQuery) {
addToast('Please select environment and enter query', 'warning');
return;
}
isRunning = true;
results = null;
try {
// Find the environment name from ID
const env = envs.find(e => e.id === selectedEnv);
const task = await runTask('search-datasets', {
env: env.name,
query: searchQuery
});
selectedTask.set(task);
startPolling(task.id);
} catch (e) {
isRunning = false;
addToast(e.message, 'error');
}
}
// [/DEF:handleSearch:Function]
// [DEF:startPolling:Function]
// @PURPOSE: Polls for task completion and results.
// @PRE: taskId is provided.
// @POST: pollInterval is set and results are updated on success.
function startPolling(taskId) {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const task = await getTaskStatus(taskId);
selectedTask.set(task);
if (task.status === 'SUCCESS') {
clearInterval(pollInterval);
isRunning = false;
results = task.result;
addToast('Search completed', 'success');
} else if (task.status === 'FAILED') {
clearInterval(pollInterval);
isRunning = false;
addToast('Search failed', 'error');
}
} catch (e) {
clearInterval(pollInterval);
isRunning = false;
addToast('Error polling task status', 'error');
}
}, 2000);
}
// [/DEF:startPolling:Function]
onMount(fetchEnvironments);
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="space-y-6">
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 class="text-lg font-medium text-gray-900 mb-4">Search Dataset Metadata</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-end">
<div>
<label for="env-select" class="block text-sm font-medium text-gray-700">Environment</label>
<select
id="env-select"
bind:value={selectedEnv}
class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
>
<option value="" disabled>-- Select Environment --</option>
{#each envs as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
</div>
<div>
<label for="search-query" class="block text-sm font-medium text-gray-700">Regex Pattern</label>
<input
type="text"
id="search-query"
bind:value={searchQuery}
placeholder="e.g. from dm.*\.account"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
</div>
<div class="mt-4 flex justify-end">
<button
on:click={handleSearch}
disabled={isRunning}
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{#if isRunning}
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Searching...
{:else}
Search
{/if}
</button>
</div>
</div>
{#if results}
<div class="bg-white shadow overflow-hidden sm:rounded-md border border-gray-200">
<div class="px-4 py-5 sm:px-6 flex justify-between items-center bg-gray-50 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Search Results
</h3>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{results.count} matches
</span>
</div>
<ul class="divide-y divide-gray-200">
{#each results.results as item}
<li class="p-4 hover:bg-gray-50">
<div class="flex items-center justify-between">
<div class="text-sm font-medium text-indigo-600 truncate">
{item.dataset_name} (ID: {item.dataset_id})
</div>
<div class="ml-2 flex-shrink-0 flex">
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Field: {item.field}
</p>
</div>
</div>
<div class="mt-2">
<pre class="text-xs text-gray-500 bg-gray-50 p-2 rounded border border-gray-100 overflow-x-auto">{item.match_context}</pre>
</div>
</li>
{/each}
{#if results.count === 0}
<li class="p-8 text-center text-gray-500 italic">
No matches found for the given pattern.
</li>
{/if}
</ul>
</div>
{/if}
</div>
<!-- [/SECTION] -->
<!-- [/DEF:SearchTool:Component] -->

View File

@@ -95,7 +95,10 @@ async function requestApi(endpoint, method = 'GET', body = null) {
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `API request failed with status ${response.status}`);
const message = errorData.detail
? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail))
: `API request failed with status ${response.status}`;
throw new Error(message);
}
return await response.json();
} catch (error) {
@@ -123,6 +126,8 @@ export const api = {
deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'),
testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}),
updateEnvironmentSchedule: (id, schedule) => requestApi(`/environments/${id}/schedule`, 'PUT', schedule),
getStorageSettings: () => fetchApi('/settings/storage'),
updateStorageSettings: (storage) => requestApi('/settings/storage', 'PUT', storage),
getEnvironmentsList: () => fetchApi('/environments'),
};
// [/DEF:api:Data]
@@ -130,6 +135,7 @@ export const api = {
// [/DEF:api_module:Module]
// Export individual functions for easier use in components
export { requestApi };
export const getPlugins = api.getPlugins;
export const getTasks = api.getTasks;
export const getTask = api.getTask;
@@ -143,3 +149,5 @@ export const deleteEnvironment = api.deleteEnvironment;
export const testEnvironmentConnection = api.testEnvironmentConnection;
export const updateEnvironmentSchedule = api.updateEnvironmentSchedule;
export const getEnvironmentsList = api.getEnvironmentsList;
export const getStorageSettings = api.getStorageSettings;
export const updateStorageSettings = api.updateStorageSettings;

View File

@@ -20,7 +20,6 @@
let settings = {
environments: [],
settings: {
backup_path: '',
default_environment_id: null,
logging: {
level: 'INFO',
@@ -204,12 +203,6 @@
<section class="mb-8 bg-white p-6 rounded shadow">
<h2 class="text-xl font-semibold mb-4">Global Settings</h2>
<div class="grid grid-cols-1 gap-4">
<div>
<label for="backup_path" class="block text-sm font-medium text-gray-700">Backup Storage Path</label>
<input type="text" id="backup_path" bind:value={settings.settings.backup_path} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div>
</div>
<h3 class="text-lg font-medium mb-4 mt-6">Logging Configuration</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">

View File

@@ -5,6 +5,8 @@
import { api } from '../lib/api.js';
import { get } from 'svelte/store';
import { goto } from '$app/navigation';
import { t } from '$lib/i18n';
import { Button, Card, PageHeader } from '$lib/ui';
/** @type {import('./$types').PageData} */
export let data;
@@ -21,10 +23,8 @@
*/
function selectPlugin(plugin) {
console.log(`[Dashboard][Action] Selecting plugin: ${plugin.id}`);
if (plugin.id === 'superset-migration') {
goto('/migration');
} else if (plugin.id === 'git-integration') {
goto('/git');
if (plugin.ui_route) {
goto(plugin.ui_route);
} else {
selectedPlugin.set(plugin);
}
@@ -55,34 +55,43 @@
<div class="container mx-auto p-4">
{#if $selectedTask}
<TaskRunner />
<button on:click={() => selectedTask.set(null)} class="mt-4 bg-blue-500 text-white p-2 rounded">
Back to Task List
</button>
<div class="mt-4">
<Button variant="primary" on:click={() => selectedTask.set(null)}>
{$t.common.cancel}
</Button>
</div>
{:else if $selectedPlugin}
<h2 class="text-2xl font-bold mb-4">{$selectedPlugin.name}</h2>
<DynamicForm schema={$selectedPlugin.schema} on:submit={handleFormSubmit} />
<button on:click={() => selectedPlugin.set(null)} class="mt-4 bg-gray-500 text-white p-2 rounded">
Back to Dashboard
</button>
<PageHeader title={$selectedPlugin.name} />
<Card>
<DynamicForm schema={$selectedPlugin.schema} on:submit={handleFormSubmit} />
</Card>
<div class="mt-4">
<Button variant="secondary" on:click={() => selectedPlugin.set(null)}>
{$t.common.cancel}
</Button>
</div>
{:else}
<h1 class="text-2xl font-bold mb-4">Available Tools</h1>
<PageHeader title={$t.nav.dashboard} />
{#if data.error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{data.error}
</div>
{/if}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each data.plugins as plugin}
<div
class="border rounded-lg p-4 cursor-pointer hover:bg-gray-100"
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each data.plugins.filter(p => p.id !== 'superset-search') as plugin}
<div
on:click={() => selectPlugin(plugin)}
role="button"
tabindex="0"
on:keydown={(e) => e.key === 'Enter' && selectPlugin(plugin)}
class="cursor-pointer transition-transform hover:scale-[1.02]"
>
<h2 class="text-xl font-semibold">{plugin.name}</h2>
<p class="text-gray-600">{plugin.description}</p>
<span class="text-sm text-gray-400">v{plugin.version}</span>
<Card title={plugin.name}>
<p class="text-gray-600 mb-4">{plugin.description}</p>
<span class="text-xs font-mono text-gray-400 bg-gray-50 px-2 py-1 rounded">v{plugin.version}</span>
</Card>
</div>
{/each}
</div>

View File

@@ -1,9 +1,16 @@
<!-- [DEF:GitDashboardPage:Component] -->
<!--
@PURPOSE: Dashboard management page for Git integration.
@LAYER: Page
@SEMANTICS: git, dashboard, management, ui
-->
<script lang="ts">
import { onMount } from 'svelte';
import DashboardGrid from '../../components/DashboardGrid.svelte';
import { addToast as toast } from '../../lib/toasts.js';
import type { DashboardMetadata } from '../../types/dashboard';
import { t } from '$lib/i18n';
import { Button, Card, PageHeader, Select } from '$lib/ui';
let environments: any[] = [];
let selectedEnvId = "";
@@ -11,6 +18,10 @@
let loading = true;
let fetchingDashboards = false;
// [DEF:fetchEnvironments:Function]
// @PURPOSE: Fetches the list of deployment environments from the API.
// @PRE: Component is mounted.
// @POST: `environments` array is populated with data from /api/environments.
async function fetchEnvironments() {
try {
const response = await fetch('/api/environments');
@@ -25,7 +36,12 @@
loading = false;
}
}
// [/DEF:fetchEnvironments:Function]
// [DEF:fetchDashboards:Function]
// @PURPOSE: Fetches dashboards for a specific environment.
// @PRE: `envId` is a valid environment ID.
// @POST: `dashboards` array is updated with results from the environment.
async function fetchDashboards(envId: string) {
if (!envId) return;
fetchingDashboards = true;
@@ -40,6 +56,7 @@
fetchingDashboards = false;
}
}
// [/DEF:fetchDashboards:Function]
onMount(fetchEnvironments);
@@ -50,29 +67,22 @@
</script>
<div class="max-w-6xl mx-auto p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">Git Dashboard Management</h1>
<div class="flex items-center space-x-4">
<label for="env-select" class="text-sm font-medium text-gray-700">Environment:</label>
<select
id="env-select"
<PageHeader title="Git Dashboard Management">
<div slot="actions" class="flex items-center space-x-4">
<Select
label="Environment"
bind:value={selectedEnvId}
class="border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2 border bg-white"
>
{#each environments as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
options={environments.map(e => ({ value: e.id, label: e.name }))}
/>
</div>
</div>
</PageHeader>
{#if loading}
<div class="flex justify-center py-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
{:else}
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-lg font-medium mb-4">Select Dashboard to Manage</h2>
<Card title="Select Dashboard to Manage">
{#if fetchingDashboards}
<p class="text-gray-500">Loading dashboards...</p>
{:else if dashboards.length > 0}
@@ -80,7 +90,7 @@
{:else}
<p class="text-gray-500 italic">No dashboards found in this environment.</p>
{/if}
</div>
</Card>
{/if}
</div>
<!-- [/DEF:GitDashboardPage:Component] -->

View File

@@ -21,6 +21,8 @@
import { selectedTask } from '../../lib/stores.js';
import { resumeTask } from '../../services/taskService.js';
import type { DashboardMetadata, DashboardSelection } from '../../types/dashboard';
import { t } from '$lib/i18n';
import { Button, Card, PageHeader } from '$lib/ui';
// [/SECTION]
// [SECTION: STATE]
@@ -294,19 +296,18 @@
<!-- [SECTION: TEMPLATE] -->
<div class="max-w-4xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-6">Migration Dashboard</h1>
<PageHeader title={$t.nav.migration} />
<TaskHistory on:viewLogs={handleViewLogs} />
{#if $selectedTask}
<div class="mt-6">
<TaskRunner />
<button
on:click={() => selectedTask.set(null)}
class="mt-4 inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Back to New Migration
</button>
<div class="mt-4">
<Button variant="secondary" on:click={() => selectedTask.set(null)}>
{$t.common.cancel}
</Button>
</div>
</div>
{:else}
{#if loading}
@@ -383,13 +384,12 @@
</div>
{/if}
<button
<Button
on:click={startMigration}
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || selectedDashboardIds.length === 0}
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-gray-400"
>
Start Migration
</button>
</Button>
{/if}
</div>

View File

@@ -14,6 +14,8 @@
import { onMount } from 'svelte';
import EnvSelector from '../../../components/EnvSelector.svelte';
import MappingTable from '../../../components/MappingTable.svelte';
import { t } from '$lib/i18n';
import { Button, PageHeader } from '$lib/ui';
// [/SECTION]
// [SECTION: STATE]
@@ -128,7 +130,7 @@
<!-- [SECTION: TEMPLATE] -->
<div class="max-w-6xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-6">Database Mapping Management</h1>
<PageHeader title="Database Mapping Management" />
{#if loading}
<p>Loading environments...</p>
@@ -149,13 +151,13 @@
</div>
<div class="mb-8">
<button
<Button
on:click={fetchDatabases}
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || fetchingDbs}
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-gray-400"
isLoading={fetchingDbs}
>
{fetchingDbs ? 'Fetching...' : 'Fetch Databases & Suggestions'}
</button>
Fetch Databases & Suggestions
</Button>
</div>
{#if error}

View File

@@ -1,7 +1,9 @@
<script>
import { onMount } from 'svelte';
import { updateGlobalSettings, addEnvironment, updateEnvironment, deleteEnvironment, testEnvironmentConnection } from '../../lib/api';
import { updateGlobalSettings, addEnvironment, updateEnvironment, deleteEnvironment, testEnvironmentConnection, updateStorageSettings } from '../../lib/api';
import { addToast } from '../../lib/toasts';
import { t } from '$lib/i18n';
import { Button, Input, Card, PageHeader } from '$lib/ui';
/** @type {import('./$types').PageData} */
export let data;
@@ -39,6 +41,24 @@
}
// [/DEF:handleSaveGlobal:Function]
// [DEF:handleSaveStorage:Function]
/* @PURPOSE: Saves storage-specific settings.
@PRE: settings.settings.storage must contain valid configuration.
@POST: Storage settings are updated via API.
*/
async function handleSaveStorage() {
try {
console.log("[Settings.handleSaveStorage][Action] Saving storage settings.");
await updateStorageSettings(settings.settings.storage);
addToast('Storage settings saved', 'success');
console.log("[Settings.handleSaveStorage][Coherence:OK] Storage settings saved.");
} catch (error) {
console.error("[Settings.handleSaveStorage][Coherence:Failed] Failed to save storage settings:", error);
addToast(error.message || 'Failed to save storage settings', 'error');
}
}
// [/DEF:handleSaveStorage:Function]
// [DEF:handleAddOrUpdateEnv:Function]
/* @PURPOSE: Adds a new environment or updates an existing one.
@PRE: newEnv must contain valid environment details.
@@ -142,7 +162,7 @@
</script>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-6">Settings</h1>
<PageHeader title={$t.settings.title} />
{#if data.error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
@@ -150,38 +170,62 @@
</div>
{/if}
<section class="mb-8 bg-white p-6 rounded shadow">
<h2 class="text-xl font-semibold mb-4">Global Settings</h2>
<div class="grid grid-cols-1 gap-4">
<div>
<label for="backup_path" class="block text-sm font-medium text-gray-700">Backup Storage Path</label>
<input type="text" id="backup_path" bind:value={settings.settings.backup_path} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
<div class="mb-8">
<Card title={$t.settings?.storage_title || "File Storage Configuration"}>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2">
<Input
label={$t.settings?.storage_root || "Storage Root Path"}
bind:value={settings.settings.storage.root_path}
/>
</div>
<Input
label={$t.settings?.storage_backup_pattern || "Backup Directory Pattern"}
bind:value={settings.settings.storage.backup_structure_pattern}
/>
<Input
label={$t.settings?.storage_repo_pattern || "Repository Directory Pattern"}
bind:value={settings.settings.storage.repo_structure_pattern}
/>
<Input
label={$t.settings?.storage_filename_pattern || "Filename Pattern"}
bind:value={settings.settings.storage.filename_pattern}
/>
<div class="bg-gray-50 p-4 rounded border border-gray-200">
<span class="block text-xs font-semibold text-gray-500 uppercase mb-2">{$t.settings?.storage_preview || "Path Preview"}</span>
<code class="text-sm text-indigo-600">
{settings.settings.storage.root_path}/backups/sample_backup.zip
</code>
</div>
</div>
<button on:click={handleSaveGlobal} class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 w-max">
Save Global Settings
</button>
<div class="mt-6">
<Button on:click={handleSaveStorage}>
{$t.common.save}
</Button>
</div>
</Card>
</div>
<section class="mb-8">
<Card title={$t.settings?.env_title || "Superset Environments"}>
{#if settings.environments.length === 0}
<div class="mb-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700">
<p class="font-bold">Warning</p>
<p>{$t.settings?.env_warning || "No Superset environments configured."}</p>
</div>
</section>
<section class="mb-8 bg-white p-6 rounded shadow">
<h2 class="text-xl font-semibold mb-4">Superset Environments</h2>
{#if settings.environments.length === 0}
<div class="mb-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700">
<p class="font-bold">Warning</p>
<p>No Superset environments configured. You must add at least one environment to perform backups or migrations.</p>
</div>
{/if}
{/if}
<div class="mb-6 overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.connections?.name || "Name"}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Username</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.connections?.user || "Username"}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Default</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.git?.actions || "Actions"}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@@ -192,9 +236,9 @@
<td class="px-6 py-4 whitespace-nowrap">{env.username}</td>
<td class="px-6 py-4 whitespace-nowrap">{env.is_default ? 'Yes' : 'No'}</td>
<td class="px-6 py-4 whitespace-nowrap">
<button on:click={() => handleTestEnv(env.id)} class="text-green-600 hover:text-green-900 mr-4">Test</button>
<button on:click={() => editEnv(env)} class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</button>
<button on:click={() => handleDeleteEnv(env.id)} class="text-red-600 hover:text-red-900">Delete</button>
<button on:click={() => handleTestEnv(env.id)} class="text-green-600 hover:text-green-900 mr-4">{$t.settings?.env_test || "Test"}</button>
<button on:click={() => editEnv(env)} class="text-indigo-600 hover:text-indigo-900 mr-4">{$t.common.edit}</button>
<button on:click={() => handleDeleteEnv(env.id)} class="text-red-600 hover:text-red-900">{$t.settings?.env_delete || "Delete"}</button>
</td>
</tr>
{/each}
@@ -202,44 +246,30 @@
</table>
</div>
<div class="bg-gray-50 p-4 rounded">
<h3 class="text-lg font-medium mb-4">{editingEnvId ? 'Edit' : 'Add'} Environment</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="env_id" class="block text-sm font-medium text-gray-700">ID</label>
<input type="text" id="env_id" bind:value={newEnv.id} disabled={!!editingEnvId} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div>
<div>
<label for="env_name" class="block text-sm font-medium text-gray-700">Name</label>
<input type="text" id="env_name" bind:value={newEnv.name} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div>
<div>
<label for="env_url" class="block text-sm font-medium text-gray-700">URL</label>
<input type="text" id="env_url" bind:value={newEnv.url} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div>
<div>
<label for="env_user" class="block text-sm font-medium text-gray-700">Username</label>
<input type="text" id="env_user" bind:value={newEnv.username} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div>
<div>
<label for="env_pass" class="block text-sm font-medium text-gray-700">Password</label>
<input type="password" id="env_pass" bind:value={newEnv.password} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div>
<div class="flex items-center">
<input type="checkbox" id="env_default" bind:checked={newEnv.is_default} class="h-4 w-4 text-blue-600 border-gray-300 rounded" />
<label for="env_default" class="ml-2 block text-sm text-gray-900">Default Environment</label>
<div class="mt-8 bg-gray-50 p-6 rounded-lg border border-gray-100">
<h3 class="text-lg font-medium mb-6">{editingEnvId ? ($t.settings?.env_edit || "Edit Environment") : ($t.settings?.env_add || "Add Environment")}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<Input label="ID" bind:value={newEnv.id} disabled={!!editingEnvId} />
<Input label={$t.connections?.name || "Name"} bind:value={newEnv.name} />
<Input label="URL" bind:value={newEnv.url} />
<Input label={$t.connections?.user || "Username"} bind:value={newEnv.username} />
<Input label={$t.connections?.pass || "Password"} type="password" bind:value={newEnv.password} />
<div class="flex items-center gap-2 h-10 mt-auto">
<input type="checkbox" id="env_default" bind:checked={newEnv.is_default} class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
<label for="env_default" class="text-sm font-medium text-gray-700">{$t.settings?.env_default || "Default Environment"}</label>
</div>
</div>
<div class="mt-4 flex gap-2">
<button on:click={handleAddOrUpdateEnv} class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
{editingEnvId ? 'Update' : 'Add'} Environment
</button>
<div class="mt-8 flex gap-3">
<Button on:click={handleAddOrUpdateEnv}>
{editingEnvId ? $t.common.save : ($t.settings?.env_add || "Add Environment")}
</Button>
{#if editingEnvId}
<button on:click={resetEnvForm} class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">
Cancel
</button>
<Button variant="secondary" on:click={resetEnvForm}>
{$t.common.cancel}
</Button>
{/if}
</div>
</div>
</Card>
</section>
</div>

View File

@@ -18,7 +18,6 @@ export async function load() {
settings: {
environments: [],
settings: {
backup_path: '',
default_environment_id: null
}
},

View File

@@ -1,40 +0,0 @@
<script>
import { onMount } from 'svelte';
import { gitService } from '../../../services/gitService';
import { addToast as toast } from '../../../lib/toasts.js';
let environments = [];
onMount(async () => {
try {
environments = await gitService.getEnvironments();
} catch (e) {
toast(e.message, 'error');
}
});
</script>
<div class="p-6">
<h1 class="text-2xl font-bold mb-6">Deployment Environments</h1>
<div class="bg-white p-6 rounded shadow">
<h2 class="text-xl font-semibold mb-4">Target Environments</h2>
{#if environments.length === 0}
<p class="text-gray-500">No deployment environments configured.</p>
{:else}
<ul class="divide-y">
{#each environments as env}
<li class="py-3 flex justify-between items-center">
<div>
<span class="font-medium">{env.name}</span>
<div class="text-xs text-gray-400">{env.superset_url}</div>
</div>
<span class="px-2 py-1 text-xs rounded {env.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}">
{env.is_active ? 'Active' : 'Inactive'}
</span>
</li>
{/each}
</ul>
{/if}
</div>
</div>

View File

@@ -1,8 +1,24 @@
<script>
<!-- [DEF:GitSettingsPage:Component] -->
<!--
@SEMANTICS: git, settings, configuration, integration
@PURPOSE: Manage Git server configurations for dashboard versioning.
@LAYER: Page
@RELATION: USES -> gitService
@RELATION: USES -> Button, Input, Card, PageHeader, Select
@INVARIANT: All configurations must be validated via connection test.
-->
<script lang="ts">
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { gitService } from '../../../services/gitService';
import { addToast as toast } from '../../../lib/toasts.js';
import { t } from '$lib/i18n';
import { Button, Input, Card, PageHeader, Select } from '$lib/ui';
// [/SECTION: IMPORTS]
// [SECTION: STATE]
let configs = [];
let newConfig = {
name: '',
@@ -12,15 +28,31 @@
default_repository: ''
};
let testing = false;
// [/SECTION: STATE]
onMount(async () => {
// [DEF:loadConfigs:Function]
/**
* @purpose Fetches existing git configurations.
* @pre Component is mounted.
* @post configs state is populated.
*/
async function loadConfigs() {
try {
configs = await gitService.getConfigs();
} catch (e) {
toast(e.message, 'error');
}
});
}
// [/DEF:loadConfigs:Function]
onMount(loadConfigs);
// [DEF:handleTest:Function]
/**
* @purpose Tests connection to a git server with current form data.
* @pre newConfig contains valid provider, url, and pat.
* @post testing state is managed; toast shown with result.
*/
async function handleTest() {
testing = true;
try {
@@ -36,7 +68,14 @@
testing = false;
}
}
// [/DEF:handleTest:Function]
// [DEF:handleSave:Function]
/**
* @purpose Saves a new git configuration.
* @pre newConfig is valid and tested.
* @post New config is saved to DB and added to configs list.
*/
async function handleSave() {
try {
const saved = await gitService.createConfig(newConfig);
@@ -47,7 +86,15 @@
toast(e.message, 'error');
}
}
// [/DEF:handleSave:Function]
// [DEF:handleDelete:Function]
/**
* @purpose Deletes a git configuration by ID.
* @param {string} id - Configuration ID.
* @pre id is valid; user confirmed deletion.
* @post Configuration is removed from DB and local state.
*/
async function handleDelete(id) {
if (!confirm('Are you sure you want to delete this Git configuration?')) return;
try {
@@ -58,31 +105,34 @@
toast(e.message, 'error');
}
}
// [/DEF:handleDelete:Function]
</script>
<div class="p-6">
<h1 class="text-2xl font-bold mb-6">Git Integration Settings</h1>
<!-- [SECTION: TEMPLATE] -->
<div class="p-6 max-w-6xl mx-auto">
<PageHeader title="Git Integration Settings" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- List of Configs -->
<div class="bg-white p-6 rounded shadow">
<h2 class="text-xl font-semibold mb-4">Configured Servers</h2>
<Card title="Configured Servers">
{#if configs.length === 0}
<p class="text-gray-500">No Git servers configured.</p>
{:else}
<ul class="divide-y">
<ul class="divide-y divide-gray-100">
{#each configs as config}
<li class="py-3 flex justify-between items-center">
<li class="py-4 flex justify-between items-center">
<div>
<span class="font-medium">{config.name}</span>
<span class="text-sm text-gray-500 ml-2">({config.provider})</span>
<div class="text-xs text-gray-400">{config.url}</div>
<div class="flex items-center gap-2">
<span class="font-medium text-gray-900">{config.name}</span>
<span class="text-xs font-mono bg-gray-50 text-gray-500 px-1.5 py-0.5 rounded">{config.provider}</span>
</div>
<div class="text-xs text-gray-400 mt-1">{config.url}</div>
</div>
<div class="flex items-center space-x-4">
<span class="px-2 py-1 text-xs rounded {config.status === 'CONNECTED' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}">
<span class="px-2 py-1 text-xs font-medium rounded {config.status === 'CONNECTED' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}">
{config.status}
</span>
<button on:click={() => handleDelete(config.id)} class="text-red-600 hover:text-red-800 p-1" title="Delete">
<button on:click={() => handleDelete(config.id)} class="text-gray-400 hover:text-red-600 transition-colors" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
@@ -92,45 +142,41 @@
{/each}
</ul>
{/if}
</div>
</Card>
<!-- Add New Config -->
<div class="bg-white p-6 rounded shadow">
<h2 class="text-xl font-semibold mb-4">Add Git Server</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Display Name</label>
<input type="text" bind:value={newConfig.name} class="mt-1 block w-full border rounded p-2" placeholder="e.g. My GitHub" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Provider</label>
<select bind:value={newConfig.provider} class="mt-1 block w-full border rounded p-2">
<option value="GITHUB">GitHub</option>
<option value="GITLAB">GitLab</option>
<option value="GITEA">Gitea</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Server URL</label>
<input type="text" bind:value={newConfig.url} class="mt-1 block w-full border rounded p-2" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Personal Access Token (PAT)</label>
<input type="password" bind:value={newConfig.pat} class="mt-1 block w-full border rounded p-2" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Default Repository (Optional)</label>
<input type="text" bind:value={newConfig.default_repository} class="mt-1 block w-full border rounded p-2" placeholder="org/repo" />
</div>
<div class="flex space-x-4 pt-4">
<button on:click={handleTest} disabled={testing} class="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300 disabled:opacity-50">
{testing ? 'Testing...' : 'Test Connection'}
</button>
<button on:click={handleSave} class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
<Card title="Add Git Server">
<div class="space-y-6">
<Input label="Display Name" bind:value={newConfig.name} placeholder="e.g. My GitHub" />
<Select
label="Provider"
bind:value={newConfig.provider}
options={[
{ value: 'GITHUB', label: 'GitHub' },
{ value: 'GITLAB', label: 'GitLab' },
{ value: 'GITEA', label: 'Gitea' }
]}
/>
<Input label="Server URL" bind:value={newConfig.url} />
<Input label="Personal Access Token (PAT)" type="password" bind:value={newConfig.pat} />
<Input label="Default Repository (Optional)" bind:value={newConfig.default_repository} placeholder="org/repo" />
<div class="flex gap-3 pt-2">
<Button variant="secondary" on:click={handleTest} isLoading={testing}>
Test Connection
</Button>
<Button variant="primary" on:click={handleSave}>
Save Configuration
</button>
</Button>
</div>
</div>
</div>
</Card>
</div>
</div>
</div>
<!-- [/SECTION: TEMPLATE] -->
<style>
/* Styles are handled by Tailwind */
</style>
<!-- [/DEF:GitSettingsPage:Component] -->

View File

@@ -12,6 +12,8 @@
import { addToast } from '../../lib/toasts';
import TaskList from '../../components/TaskList.svelte';
import TaskLogViewer from '../../components/TaskLogViewer.svelte';
import { t } from '$lib/i18n';
import { Button, Card, PageHeader, Select } from '$lib/ui';
let tasks = [];
let environments = [];
@@ -114,35 +116,29 @@
</script>
<div class="container mx-auto p-4 max-w-6xl">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">Task Management</h1>
<button
on:click={() => showBackupModal = true}
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md shadow-sm transition duration-150 font-medium"
>
Run Backup
</button>
</div>
<PageHeader title={$t.tasks.management} />
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-1">
<h2 class="text-lg font-semibold mb-3 text-gray-700">Recent Tasks</h2>
<h2 class="text-lg font-semibold mb-3 text-gray-700">{$t.tasks.recent}</h2>
<TaskList {tasks} {loading} on:select={handleSelectTask} />
</div>
<div class="lg:col-span-2">
<h2 class="text-lg font-semibold mb-3 text-gray-700">Task Details & Logs</h2>
<h2 class="text-lg font-semibold mb-3 text-gray-700">{$t.tasks.details_logs}</h2>
{#if selectedTaskId}
<div class="bg-white rounded-lg shadow-lg h-[600px] flex flex-col">
<TaskLogViewer
taskId={selectedTaskId}
taskStatus={tasks.find(t => t.id === selectedTaskId)?.status}
inline={true}
/>
</div>
<Card padding="none">
<div class="h-[600px] flex flex-col overflow-hidden rounded-lg">
<TaskLogViewer
taskId={selectedTaskId}
taskStatus={tasks.find(t => t.id === selectedTaskId)?.status}
inline={true}
/>
</div>
</Card>
{:else}
<div class="bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg h-[600px] flex items-center justify-center text-gray-500">
<p>Select a task to view logs and details</p>
<div class="bg-gray-50 border-2 border-dashed border-gray-100 rounded-lg h-[600px] flex items-center justify-center text-gray-400">
<p>{$t.tasks.select_task}</p>
</div>
{/if}
</div>
@@ -150,36 +146,29 @@
</div>
{#if showBackupModal}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<h3 class="text-xl font-bold mb-4">Run Manual Backup</h3>
<div class="mb-4">
<label for="env-select" class="block text-sm font-medium text-gray-700 mb-1">Target Environment</label>
<select
id="env-select"
bind:value={selectedEnvId}
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2 border"
>
<option value="" disabled>-- Select Environment --</option>
{#each environments as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
</div>
<div class="flex justify-end space-x-3">
<button
on:click={() => showBackupModal = false}
class="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md transition"
>
Cancel
</button>
<button
on:click={handleRunBackup}
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition"
>
Start Backup
</button>
</div>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/50 backdrop-blur-sm p-4">
<div class="w-full max-w-md">
<Card title={$t.tasks.manual_backup}>
<div class="space-y-6">
<Select
label={$t.tasks.target_env}
bind:value={selectedEnvId}
options={[
{ value: '', label: $t.tasks.select_env },
...environments.map(e => ({ value: e.id, label: e.name }))
]}
/>
<div class="flex justify-end gap-3 pt-2">
<Button variant="secondary" on:click={() => showBackupModal = false}>
{$t.common.cancel}
</Button>
<Button variant="primary" on:click={handleRunBackup}>
Start Backup
</Button>
</div>
</div>
</Card>
</div>
</div>
{/if}

View File

@@ -0,0 +1,27 @@
<!-- [DEF:BackupPage:Component] -->
<!--
@SEMANTICS: backup, page, tools
@PURPOSE: Entry point for the Backup Management interface.
@LAYER: Page
@RELATION: USES -> BackupManager
-->
<script lang="ts">
// [SECTION: IMPORTS]
import { t } from '../../../lib/i18n';
import { PageHeader } from '../../../lib/ui';
import BackupManager from '../../../components/backups/BackupManager.svelte';
// [/SECTION]
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="container mx-auto p-4 max-w-6xl">
<PageHeader title={$t.nav.tools_backups} />
<div class="mt-6">
<BackupManager />
</div>
</div>
<!-- [/SECTION] -->
<!-- [/DEF:BackupPage:Component] -->

View File

@@ -7,19 +7,18 @@
<script>
import DebugTool from '../../../components/tools/DebugTool.svelte';
import TaskRunner from '../../../components/TaskRunner.svelte';
import { PageHeader } from '$lib/ui';
</script>
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">System Diagnostics</h1>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2">
<DebugTool />
</div>
<div class="lg:col-span-1">
<TaskRunner />
</div>
<div class="max-w-7xl mx-auto p-6">
<PageHeader title="System Diagnostics" />
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2">
<DebugTool />
</div>
<div class="lg:col-span-1">
<TaskRunner />
</div>
</div>
</div>

View File

@@ -7,19 +7,18 @@
<script>
import MapperTool from '../../../components/tools/MapperTool.svelte';
import TaskRunner from '../../../components/TaskRunner.svelte';
import { PageHeader } from '$lib/ui';
</script>
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Dataset Column Mapper</h1>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2">
<MapperTool />
</div>
<div class="lg:col-span-1">
<TaskRunner />
</div>
<div class="max-w-7xl mx-auto p-6">
<PageHeader title="Dataset Column Mapper" />
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2">
<MapperTool />
</div>
<div class="lg:col-span-1">
<TaskRunner />
</div>
</div>
</div>

View File

@@ -1,26 +0,0 @@
<!-- [DEF:SearchPage:Component] -->
<!--
@SEMANTICS: search, page, tool
@PURPOSE: Page for the dataset search tool.
@LAYER: UI
-->
<script>
import SearchTool from '../../../components/tools/SearchTool.svelte';
import TaskRunner from '../../../components/TaskRunner.svelte';
</script>
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Dataset Search</h1>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2">
<SearchTool />
</div>
<div class="lg:col-span-1">
<TaskRunner />
</div>
</div>
</div>
</div>
<!-- [/DEF:SearchPage:Component] -->

View File

@@ -0,0 +1,212 @@
<!-- [DEF:StoragePage:Component] -->
<!--
@SEMANTICS: storage, files, management
@PURPOSE: Main page for file storage management.
@LAYER: Feature
@RELATION: DEPENDS_ON -> storageService
@RELATION: CONTAINS -> FileList
@RELATION: CONTAINS -> FileUpload
@INVARIANT: Always displays tabs for Backups and Repositories.
-->
<script lang="ts">
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { listFiles, deleteFile } from '../../../services/storageService';
import { addToast } from '../../../lib/toasts';
import { t } from '../../../lib/i18n';
import FileList from '../../../components/storage/FileList.svelte';
import FileUpload from '../../../components/storage/FileUpload.svelte';
// [/SECTION: IMPORTS]
// [DEF:loadFiles:Function]
/**
* @purpose Fetches the list of files from the server.
* @post Updates the `files` array with the latest data.
*/
let files = [];
let isLoading = false;
let activeTab = 'backups';
let currentPath = 'backups'; // Relative to storage root
async function loadFiles() {
isLoading = true;
try {
const category = activeTab;
// If we have a currentPath, we use it.
// But if user switched tabs, we should reset currentPath to category root
let effectivePath = currentPath;
if (category && !currentPath.startsWith(category)) {
effectivePath = category;
currentPath = category;
}
// API expects path relative to category root if category is provided
const subpath = (category && effectivePath.startsWith(category))
? effectivePath.substring(category.length).replace(/^\/+/, '')
: effectivePath;
files = await listFiles(category, subpath);
} catch (error) {
addToast($t.storage.messages.load_failed.replace('{error}', error.message), 'error');
} finally {
isLoading = false;
}
}
// [/DEF:loadFiles:Function]
// [DEF:handleDelete:Function]
/**
* @purpose Handles the file deletion process.
* @param {CustomEvent} event - The delete event containing category and path.
*/
async function handleDelete(event) {
const { category, path, name } = event.detail;
if (!confirm($t.storage.messages.delete_confirm.replace('{name}', name))) return;
try {
await deleteFile(category, path);
addToast($t.storage.messages.delete_success.replace('{name}', name), 'success');
await loadFiles();
} catch (error) {
addToast($t.storage.messages.delete_failed.replace('{error}', error.message), 'error');
}
}
// [/DEF:handleDelete:Function]
// [DEF:handleNavigate:Function]
/**
* @purpose Updates the current path and reloads files when navigating into a directory.
* @param {CustomEvent} event - The navigation event containing the new path.
*/
function handleNavigate(event) {
currentPath = event.detail;
loadFiles();
}
// [/DEF:handleNavigate:Function]
// [DEF:navigateUp:Function]
/**
* @purpose Navigates one level up in the directory structure.
* @pre currentPath is set and deeper than activeTab root.
* @post currentPath is moved up one directory level.
*/
function navigateUp() {
if (!currentPath || currentPath === activeTab) return;
const parts = currentPath.split('/');
parts.pop();
currentPath = parts.join('/') || '';
loadFiles();
}
// [/DEF:navigateUp:Function]
onMount(() => {
const pathParam = $page.url.searchParams.get('path');
if (pathParam) {
currentPath = pathParam;
if (pathParam.startsWith('repositorys')) {
activeTab = 'repositorys';
} else {
activeTab = 'backups';
}
}
loadFiles();
});
$: if (activeTab) {
// Reset path when switching tabs
if (!currentPath.startsWith(activeTab)) {
currentPath = activeTab;
}
loadFiles();
}
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="container mx-auto p-4 max-w-6xl">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">{$t.storage.management}</h1>
{#if currentPath}
<div class="flex items-center mt-2 text-sm text-gray-500">
<button on:click={() => { currentPath = activeTab; loadFiles(); }} class="hover:text-indigo-600">{$t.storage.root}</button>
{#each currentPath.split('/').slice(1) as part, i}
<span class="mx-2">/</span>
<button
on:click={() => { currentPath = currentPath.split('/').slice(0, i + 1).join('/'); loadFiles(); }}
class="hover:text-indigo-600 capitalize"
>
{part}
</button>
{/each}
</div>
{/if}
</div>
<div class="flex justify-end mb-4">
<button
on:click={loadFiles}
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
disabled={isLoading}
>
{isLoading ? $t.storage.refreshing : $t.storage.refresh}
</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Main Content: File List -->
<div class="lg:col-span-2 space-y-4">
<!-- Tabs -->
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-8">
<button
on:click={() => activeTab = 'backups'}
class="py-4 px-1 border-b-2 font-medium text-sm {activeTab === 'backups' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}"
>
{$t.storage.backups}
</button>
<button
on:click={() => activeTab = 'repositorys'}
class="py-4 px-1 border-b-2 font-medium text-sm {activeTab === 'repositorys' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}"
>
{$t.storage.repositories}
</button>
</nav>
</div>
<div class="flex items-center mb-2">
{#if currentPath && currentPath !== activeTab}
<button
on:click={navigateUp}
class="mr-4 inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50"
>
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back
</button>
{/if}
</div>
<FileList {files} on:delete={handleDelete} on:navigate={handleNavigate} />
</div>
<!-- Sidebar: Upload -->
<div class="lg:col-span-1">
<FileUpload
category={activeTab}
path={currentPath}
on:uploaded={loadFiles}
/>
</div>
</div>
</div>
<!-- [/SECTION: TEMPLATE] -->
<style>
/* ... */
</style>
<!-- [/DEF:StoragePage:Component] -->

View File

@@ -1,5 +1,5 @@
// [DEF:GitServiceClient:Module]
/**
* [DEF:GitServiceClient:Module]
* @SEMANTICS: git, service, api, client
* @PURPOSE: API client for Git operations, managing the communication between frontend and backend.
* @LAYER: Service

View File

@@ -0,0 +1,109 @@
// [DEF:storageService:Module]
/**
* @purpose Frontend API client for file storage management.
* @layer Service
* @relation DEPENDS_ON -> backend.api.storage
* @SEMANTICS: storage, api, client
*/
const API_BASE = '/api/storage';
// [DEF:listFiles:Function]
/**
* @purpose Fetches the list of files for a given category and subpath.
* @param {string} [category] - Optional category filter.
* @param {string} [path] - Optional subpath filter.
* @returns {Promise<Array>}
* @PRE category and path should be valid strings if provided.
* @POST Returns a promise resolving to an array of StoredFile objects.
*/
export async function listFiles(category, path) {
const params = new URLSearchParams();
if (category) {
params.append('category', category);
}
if (path) {
params.append('path', path);
}
const response = await fetch(`${API_BASE}/files?${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to fetch files: ${response.statusText}`);
}
return await response.json();
}
// [/DEF:listFiles:Function]
// [DEF:uploadFile:Function]
/**
* @purpose Uploads a file to the storage system.
* @param {File} file - The file to upload.
* @param {string} category - Target category.
* @param {string} [path] - Target subpath.
* @returns {Promise<Object>}
* @PRE file must be a valid File object; category must be specified.
* @POST Returns a promise resolving to the metadata of the uploaded file.
*/
export async function uploadFile(file, category, path) {
const formData = new FormData();
formData.append('file', file);
formData.append('category', category);
if (path) {
formData.append('path', path);
}
const response = await fetch(`${API_BASE}/upload`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Failed to upload file: ${response.statusText}`);
}
return await response.json();
}
// [/DEF:uploadFile:Function]
// [DEF:deleteFile:Function]
/**
* @purpose Deletes a file or directory from storage.
* @param {string} category - File category.
* @param {string} path - Relative path of the item.
* @returns {Promise<void>}
* @PRE category and path must identify an existing file or directory.
* @POST The specified file or directory is removed from storage.
*/
export async function deleteFile(category, path) {
const response = await fetch(`${API_BASE}/files/${category}/${path}`, {
method: 'DELETE'
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Failed to delete: ${response.statusText}`);
}
}
// [/DEF:deleteFile:Function]
// [DEF:downloadFileUrl:Function]
/**
* @purpose Returns the URL for downloading a file.
* @param {string} category - File category.
* @param {string} path - Relative path of the file.
* @returns {string}
* @PRE category and path must identify an existing file.
* @POST Returns a valid API URL for file download.
*/
export function downloadFileUrl(category, path) {
return `${API_BASE}/download/${category}/${path}`;
}
// [/DEF:downloadFileUrl:Function]
export default {
listFiles,
uploadFile,
deleteFile,
downloadFileUrl
};
// [/DEF:storageService:Module]

View File

@@ -0,0 +1,22 @@
/**
* [DEF:BackupTypes:Module]
* @SEMANTICS: types, backup, interface
* @PURPOSE: Defines types and interfaces for the Backup Management UI.
*/
export interface Backup {
id: string;
name: string;
environment: string;
created_at: string;
size_bytes?: number;
status: 'success' | 'failed' | 'in_progress';
}
export interface BackupCreateRequest {
environment_id: string;
}
/**
* [/DEF:BackupTypes:Module]
*/

View File

@@ -130,7 +130,8 @@ class SemanticEntity:
self.compliance_issues.append(f"Missing Mandatory Tag: @{req_tag}")
# 3. Check for Belief State Logging (Python only)
if self.type == "Function" and self.file_path.endswith(".py"):
# Skip check for logger.py to avoid circular dependencies
if self.type == "Function" and self.file_path.endswith(".py") and "backend/src/core/logger.py" not in self.file_path:
if not getattr(self, 'has_belief_scope', False):
self.compliance_issues.append("Missing Belief State Logging: Function should use belief_scope context manager.")

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,109 @@
# Semantic Compliance Report
**Generated At:** 2026-01-23T21:49:38.910491
**Global Compliance Score:** 96.7%
**Scanned Files:** 93
## Critical Parsing Errors
- 🔴 backend/src/core/utils/network.py:27 Function '__init__' implementation found without matching [DEF:__init__:Function] contract.
- 🔴 backend/src/core/utils/network.py:38 Function '__init__' implementation found without matching [DEF:__init__:Function] contract.
- 🔴 backend/src/core/utils/network.py:48 Function '__init__' implementation found without matching [DEF:__init__:Function] contract.
- 🔴 backend/src/core/utils/network.py:58 Function '__init__' implementation found without matching [DEF:__init__:Function] contract.
- 🔴 backend/src/core/utils/network.py:68 Function '__init__' implementation found without matching [DEF:__init__:Function] contract.
## File Compliance Status
| File | Score | Issues |
|------|-------|--------|
| backend/src/api/routes/git_schemas.py | 🟡 54% | [GitServerConfigBase] Missing Mandatory Tag: @PURPOSE<br>[GitServerConfigBase] Missing Mandatory Tag: @PURPOSE<br>[GitServerConfigCreate] Missing Mandatory Tag: @PURPOSE<br>[GitServerConfigCreate] Missing Mandatory Tag: @PURPOSE<br>[GitServerConfigSchema] Missing Mandatory Tag: @PURPOSE<br>[GitServerConfigSchema] Missing Mandatory Tag: @PURPOSE<br>[GitRepositorySchema] Missing Mandatory Tag: @PURPOSE<br>[GitRepositorySchema] Missing Mandatory Tag: @PURPOSE<br>[BranchSchema] Missing Mandatory Tag: @PURPOSE<br>[BranchSchema] Missing Mandatory Tag: @PURPOSE<br>[CommitSchema] Missing Mandatory Tag: @PURPOSE<br>[CommitSchema] Missing Mandatory Tag: @PURPOSE<br>[BranchCreate] Missing Mandatory Tag: @PURPOSE<br>[BranchCreate] Missing Mandatory Tag: @PURPOSE<br>[BranchCheckout] Missing Mandatory Tag: @PURPOSE<br>[BranchCheckout] Missing Mandatory Tag: @PURPOSE<br>[CommitCreate] Missing Mandatory Tag: @PURPOSE<br>[CommitCreate] Missing Mandatory Tag: @PURPOSE<br>[ConflictResolution] Missing Mandatory Tag: @PURPOSE<br>[ConflictResolution] Missing Mandatory Tag: @PURPOSE<br>[DeploymentEnvironmentSchema] Missing Mandatory Tag: @PURPOSE<br>[DeploymentEnvironmentSchema] Missing Mandatory Tag: @PURPOSE<br>[DeployRequest] Missing Mandatory Tag: @PURPOSE<br>[DeployRequest] Missing Mandatory Tag: @PURPOSE<br>[RepoInitRequest] Missing Mandatory Tag: @PURPOSE<br>[RepoInitRequest] Missing Mandatory Tag: @PURPOSE |
| frontend/src/components/git/GitManager.svelte | 🟡 67% | [checkStatus] Missing Mandatory Tag: @PRE<br>[checkStatus] Missing Mandatory Tag: @POST<br>[checkStatus] Missing Mandatory Tag: @PRE<br>[checkStatus] Missing Mandatory Tag: @POST<br>[handleInit] Missing Mandatory Tag: @PRE<br>[handleInit] Missing Mandatory Tag: @POST<br>[handleInit] Missing Mandatory Tag: @PRE<br>[handleInit] Missing Mandatory Tag: @POST<br>[handleSync] Missing Mandatory Tag: @PRE<br>[handleSync] Missing Mandatory Tag: @POST<br>[handleSync] Missing Mandatory Tag: @PRE<br>[handleSync] Missing Mandatory Tag: @POST<br>[handlePush] Missing Mandatory Tag: @PURPOSE<br>[handlePush] Missing Mandatory Tag: @PRE<br>[handlePush] Missing Mandatory Tag: @POST<br>[handlePush] Missing Mandatory Tag: @PURPOSE<br>[handlePush] Missing Mandatory Tag: @PRE<br>[handlePush] Missing Mandatory Tag: @POST<br>[handlePull] Missing Mandatory Tag: @PURPOSE<br>[handlePull] Missing Mandatory Tag: @PRE<br>[handlePull] Missing Mandatory Tag: @POST<br>[handlePull] Missing Mandatory Tag: @PURPOSE<br>[handlePull] Missing Mandatory Tag: @PRE<br>[handlePull] Missing Mandatory Tag: @POST |
| frontend/src/routes/git/+page.svelte | 🟡 67% | [fetchEnvironments] Missing Mandatory Tag: @PURPOSE<br>[fetchEnvironments] Missing Mandatory Tag: @PRE<br>[fetchEnvironments] Missing Mandatory Tag: @POST<br>[fetchEnvironments] Missing Mandatory Tag: @PURPOSE<br>[fetchEnvironments] Missing Mandatory Tag: @PRE<br>[fetchEnvironments] Missing Mandatory Tag: @POST<br>[fetchDashboards] Missing Mandatory Tag: @PURPOSE<br>[fetchDashboards] Missing Mandatory Tag: @PRE<br>[fetchDashboards] Missing Mandatory Tag: @POST<br>[fetchDashboards] Missing Mandatory Tag: @PURPOSE<br>[fetchDashboards] Missing Mandatory Tag: @PRE<br>[fetchDashboards] Missing Mandatory Tag: @POST |
| backend/src/api/routes/git.py | 🟡 69% | [get_git_configs] Missing Mandatory Tag: @PRE<br>[get_git_configs] Missing Mandatory Tag: @POST<br>[get_git_configs] Missing Mandatory Tag: @PRE<br>[get_git_configs] Missing Mandatory Tag: @POST<br>[create_git_config] Missing Mandatory Tag: @PRE<br>[create_git_config] Missing Mandatory Tag: @POST<br>[create_git_config] Missing Mandatory Tag: @PRE<br>[create_git_config] Missing Mandatory Tag: @POST<br>[delete_git_config] Missing Mandatory Tag: @PRE<br>[delete_git_config] Missing Mandatory Tag: @POST<br>[delete_git_config] Missing Mandatory Tag: @PRE<br>[delete_git_config] Missing Mandatory Tag: @POST<br>[test_git_config] Missing Mandatory Tag: @PRE<br>[test_git_config] Missing Mandatory Tag: @POST<br>[test_git_config] Missing Mandatory Tag: @PRE<br>[test_git_config] Missing Mandatory Tag: @POST<br>[init_repository] Missing Mandatory Tag: @PRE<br>[init_repository] Missing Mandatory Tag: @POST<br>[init_repository] Missing Mandatory Tag: @PRE<br>[init_repository] Missing Mandatory Tag: @POST<br>[get_branches] Missing Mandatory Tag: @PRE<br>[get_branches] Missing Mandatory Tag: @POST<br>[get_branches] Missing Mandatory Tag: @PRE<br>[get_branches] Missing Mandatory Tag: @POST<br>[create_branch] Missing Mandatory Tag: @PRE<br>[create_branch] Missing Mandatory Tag: @POST<br>[create_branch] Missing Mandatory Tag: @PRE<br>[create_branch] Missing Mandatory Tag: @POST<br>[checkout_branch] Missing Mandatory Tag: @PRE<br>[checkout_branch] Missing Mandatory Tag: @POST<br>[checkout_branch] Missing Mandatory Tag: @PRE<br>[checkout_branch] Missing Mandatory Tag: @POST<br>[commit_changes] Missing Mandatory Tag: @PRE<br>[commit_changes] Missing Mandatory Tag: @POST<br>[commit_changes] Missing Mandatory Tag: @PRE<br>[commit_changes] Missing Mandatory Tag: @POST<br>[push_changes] Missing Mandatory Tag: @PRE<br>[push_changes] Missing Mandatory Tag: @POST<br>[push_changes] Missing Mandatory Tag: @PRE<br>[push_changes] Missing Mandatory Tag: @POST<br>[pull_changes] Missing Mandatory Tag: @PRE<br>[pull_changes] Missing Mandatory Tag: @POST<br>[pull_changes] Missing Mandatory Tag: @PRE<br>[pull_changes] Missing Mandatory Tag: @POST<br>[sync_dashboard] Missing Mandatory Tag: @PRE<br>[sync_dashboard] Missing Mandatory Tag: @POST<br>[sync_dashboard] Missing Mandatory Tag: @PRE<br>[sync_dashboard] Missing Mandatory Tag: @POST<br>[get_environments] Missing Mandatory Tag: @PRE<br>[get_environments] Missing Mandatory Tag: @POST<br>[get_environments] Missing Mandatory Tag: @PRE<br>[get_environments] Missing Mandatory Tag: @POST<br>[deploy_dashboard] Missing Mandatory Tag: @PRE<br>[deploy_dashboard] Missing Mandatory Tag: @POST<br>[deploy_dashboard] Missing Mandatory Tag: @PRE<br>[deploy_dashboard] Missing Mandatory Tag: @POST<br>[get_history] Missing Mandatory Tag: @PRE<br>[get_history] Missing Mandatory Tag: @POST<br>[get_history] Missing Mandatory Tag: @PRE<br>[get_history] Missing Mandatory Tag: @POST<br>[get_repository_status] Missing Mandatory Tag: @PRE<br>[get_repository_status] Missing Mandatory Tag: @POST<br>[get_repository_status] Missing Mandatory Tag: @PRE<br>[get_repository_status] Missing Mandatory Tag: @POST<br>[get_repository_diff] Missing Mandatory Tag: @PRE<br>[get_repository_diff] Missing Mandatory Tag: @POST<br>[get_repository_diff] Missing Mandatory Tag: @PRE<br>[get_repository_diff] Missing Mandatory Tag: @POST |
| backend/src/services/git_service.py | 🟡 72% | [__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Mandatory Tag: @POST<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Mandatory Tag: @POST<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Mandatory Tag: @POST<br>[_get_repo_path] Missing Mandatory Tag: @PRE<br>[_get_repo_path] Missing Mandatory Tag: @POST<br>[_get_repo_path] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_get_repo_path] Missing Mandatory Tag: @PRE<br>[_get_repo_path] Missing Mandatory Tag: @POST<br>[_get_repo_path] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_get_repo_path] Missing Mandatory Tag: @PRE<br>[_get_repo_path] Missing Mandatory Tag: @POST<br>[_get_repo_path] Missing Belief State Logging: Function should use belief_scope context manager.<br>[init_repo] Missing Mandatory Tag: @PRE<br>[init_repo] Missing Mandatory Tag: @POST<br>[init_repo] Missing Mandatory Tag: @PRE<br>[init_repo] Missing Mandatory Tag: @POST<br>[init_repo] Missing Mandatory Tag: @PRE<br>[init_repo] Missing Mandatory Tag: @POST<br>[get_repo] Missing Mandatory Tag: @POST<br>[get_repo] Missing Mandatory Tag: @POST<br>[get_repo] Missing Mandatory Tag: @POST<br>[list_branches] Missing Mandatory Tag: @PRE<br>[list_branches] Missing Mandatory Tag: @POST<br>[list_branches] Missing Mandatory Tag: @PRE<br>[list_branches] Missing Mandatory Tag: @POST<br>[list_branches] Missing Mandatory Tag: @PRE<br>[list_branches] Missing Mandatory Tag: @POST<br>[create_branch] Missing Mandatory Tag: @PRE<br>[create_branch] Missing Mandatory Tag: @POST<br>[create_branch] Missing Mandatory Tag: @PRE<br>[create_branch] Missing Mandatory Tag: @POST<br>[create_branch] Missing Mandatory Tag: @PRE<br>[create_branch] Missing Mandatory Tag: @POST<br>[checkout_branch] Missing Mandatory Tag: @PRE<br>[checkout_branch] Missing Mandatory Tag: @POST<br>[checkout_branch] Missing Mandatory Tag: @PRE<br>[checkout_branch] Missing Mandatory Tag: @POST<br>[checkout_branch] Missing Mandatory Tag: @PRE<br>[checkout_branch] Missing Mandatory Tag: @POST<br>[commit_changes] Missing Mandatory Tag: @PRE<br>[commit_changes] Missing Mandatory Tag: @POST<br>[commit_changes] Missing Mandatory Tag: @PRE<br>[commit_changes] Missing Mandatory Tag: @POST<br>[commit_changes] Missing Mandatory Tag: @PRE<br>[commit_changes] Missing Mandatory Tag: @POST<br>[push_changes] Missing Mandatory Tag: @PRE<br>[push_changes] Missing Mandatory Tag: @POST<br>[push_changes] Missing Mandatory Tag: @PRE<br>[push_changes] Missing Mandatory Tag: @POST<br>[push_changes] Missing Mandatory Tag: @PRE<br>[push_changes] Missing Mandatory Tag: @POST<br>[pull_changes] Missing Mandatory Tag: @PRE<br>[pull_changes] Missing Mandatory Tag: @POST<br>[pull_changes] Missing Mandatory Tag: @PRE<br>[pull_changes] Missing Mandatory Tag: @POST<br>[pull_changes] Missing Mandatory Tag: @PRE<br>[pull_changes] Missing Mandatory Tag: @POST<br>[get_status] Missing Mandatory Tag: @PRE<br>[get_status] Missing Mandatory Tag: @POST<br>[get_status] Missing Mandatory Tag: @PRE<br>[get_status] Missing Mandatory Tag: @POST<br>[get_status] Missing Mandatory Tag: @PRE<br>[get_status] Missing Mandatory Tag: @POST<br>[get_diff] Missing Mandatory Tag: @PRE<br>[get_diff] Missing Mandatory Tag: @POST<br>[get_diff] Missing Mandatory Tag: @PRE<br>[get_diff] Missing Mandatory Tag: @POST<br>[get_diff] Missing Mandatory Tag: @PRE<br>[get_diff] Missing Mandatory Tag: @POST<br>[get_commit_history] Missing Mandatory Tag: @PRE<br>[get_commit_history] Missing Mandatory Tag: @POST<br>[get_commit_history] Missing Mandatory Tag: @PRE<br>[get_commit_history] Missing Mandatory Tag: @POST<br>[get_commit_history] Missing Mandatory Tag: @PRE<br>[get_commit_history] Missing Mandatory Tag: @POST<br>[test_connection] Missing Mandatory Tag: @PRE<br>[test_connection] Missing Mandatory Tag: @POST<br>[test_connection] Missing Mandatory Tag: @PRE<br>[test_connection] Missing Mandatory Tag: @POST<br>[test_connection] Missing Mandatory Tag: @PRE<br>[test_connection] Missing Mandatory Tag: @POST |
| frontend/src/routes/settings/git/+page.svelte | 🟡 73% | [loadConfigs] Missing Mandatory Tag: @PRE<br>[loadConfigs] Missing Mandatory Tag: @POST<br>[loadConfigs] Missing Mandatory Tag: @PRE<br>[loadConfigs] Missing Mandatory Tag: @POST<br>[handleTest] Missing Mandatory Tag: @PRE<br>[handleTest] Missing Mandatory Tag: @POST<br>[handleTest] Missing Mandatory Tag: @PRE<br>[handleTest] Missing Mandatory Tag: @POST<br>[handleSave] Missing Mandatory Tag: @PRE<br>[handleSave] Missing Mandatory Tag: @POST<br>[handleSave] Missing Mandatory Tag: @PRE<br>[handleSave] Missing Mandatory Tag: @POST<br>[handleDelete] Missing Mandatory Tag: @PRE<br>[handleDelete] Missing Mandatory Tag: @POST<br>[handleDelete] Missing Mandatory Tag: @PRE<br>[handleDelete] Missing Mandatory Tag: @POST |
| frontend/src/components/git/BranchSelector.svelte | 🟡 75% | [onMount] Missing Mandatory Tag: @PURPOSE<br>[onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[onMount] Missing Mandatory Tag: @PURPOSE<br>[onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[loadBranches] Missing Mandatory Tag: @PRE<br>[loadBranches] Missing Mandatory Tag: @PRE<br>[handleSelect] Missing Mandatory Tag: @PURPOSE<br>[handleSelect] Missing Mandatory Tag: @PRE<br>[handleSelect] Missing Mandatory Tag: @POST<br>[handleSelect] Missing Mandatory Tag: @PURPOSE<br>[handleSelect] Missing Mandatory Tag: @PRE<br>[handleSelect] Missing Mandatory Tag: @POST<br>[handleCheckout] Missing Mandatory Tag: @PRE<br>[handleCheckout] Missing Mandatory Tag: @PRE<br>[handleCreate] Missing Mandatory Tag: @PRE<br>[handleCreate] Missing Mandatory Tag: @PRE |
| backend/src/plugins/git_plugin.py | 🟡 82% | [__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Mandatory Tag: @PRE<br>[id] Missing Mandatory Tag: @PRE<br>[id] Missing Mandatory Tag: @POST<br>[id] Missing Belief State Logging: Function should use belief_scope context manager.<br>[id] Missing Mandatory Tag: @PRE<br>[id] Missing Mandatory Tag: @POST<br>[id] Missing Belief State Logging: Function should use belief_scope context manager.<br>[id] Missing Mandatory Tag: @PRE<br>[id] Missing Mandatory Tag: @POST<br>[id] Missing Belief State Logging: Function should use belief_scope context manager.<br>[name] Missing Mandatory Tag: @PRE<br>[name] Missing Mandatory Tag: @POST<br>[name] Missing Belief State Logging: Function should use belief_scope context manager.<br>[name] Missing Mandatory Tag: @PRE<br>[name] Missing Mandatory Tag: @POST<br>[name] Missing Belief State Logging: Function should use belief_scope context manager.<br>[name] Missing Mandatory Tag: @PRE<br>[name] Missing Mandatory Tag: @POST<br>[name] Missing Belief State Logging: Function should use belief_scope context manager.<br>[description] Missing Mandatory Tag: @PRE<br>[description] Missing Mandatory Tag: @POST<br>[description] Missing Belief State Logging: Function should use belief_scope context manager.<br>[description] Missing Mandatory Tag: @PRE<br>[description] Missing Mandatory Tag: @POST<br>[description] Missing Belief State Logging: Function should use belief_scope context manager.<br>[description] Missing Mandatory Tag: @PRE<br>[description] Missing Mandatory Tag: @POST<br>[description] Missing Belief State Logging: Function should use belief_scope context manager.<br>[version] Missing Mandatory Tag: @PRE<br>[version] Missing Mandatory Tag: @POST<br>[version] Missing Belief State Logging: Function should use belief_scope context manager.<br>[version] Missing Mandatory Tag: @PRE<br>[version] Missing Mandatory Tag: @POST<br>[version] Missing Belief State Logging: Function should use belief_scope context manager.<br>[version] Missing Mandatory Tag: @PRE<br>[version] Missing Mandatory Tag: @POST<br>[version] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_schema] Missing Mandatory Tag: @PRE<br>[get_schema] Missing Mandatory Tag: @POST<br>[get_schema] Missing Mandatory Tag: @PRE<br>[get_schema] Missing Mandatory Tag: @POST<br>[get_schema] Missing Mandatory Tag: @PRE<br>[get_schema] Missing Mandatory Tag: @POST<br>[initialize] Missing Mandatory Tag: @PRE<br>[initialize] Missing Mandatory Tag: @PRE<br>[initialize] Missing Mandatory Tag: @PRE<br>[_get_env] Missing Mandatory Tag: @PRE<br>[_get_env] Missing Mandatory Tag: @POST<br>[_get_env] Missing Mandatory Tag: @PRE<br>[_get_env] Missing Mandatory Tag: @POST<br>[_get_env] Missing Mandatory Tag: @PRE<br>[_get_env] Missing Mandatory Tag: @POST<br>[_get_env] Missing Mandatory Tag: @PRE<br>[_get_env] Missing Mandatory Tag: @POST |
| backend/src/models/git.py | 🟡 83% | [GitModels] Missing Mandatory Tag: @SEMANTICS |
| frontend/src/components/git/CommitHistory.svelte | 🟡 83% | [onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[loadHistory] Missing Mandatory Tag: @PRE<br>[loadHistory] Missing Mandatory Tag: @PRE |
| backend/src/core/utils/network.py | 🟡 93% | [SupersetAPIError.__init__] Missing Mandatory Tag: @PRE<br>[SupersetAPIError.__init__] Missing Mandatory Tag: @POST<br>[SupersetAPIError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[SupersetAPIError.__init__] Missing Mandatory Tag: @PRE<br>[SupersetAPIError.__init__] Missing Mandatory Tag: @POST<br>[SupersetAPIError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[SupersetAPIError.__init__] Missing Mandatory Tag: @PRE<br>[SupersetAPIError.__init__] Missing Mandatory Tag: @POST<br>[SupersetAPIError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[AuthenticationError.__init__] Missing Mandatory Tag: @PRE<br>[AuthenticationError.__init__] Missing Mandatory Tag: @POST<br>[AuthenticationError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[AuthenticationError.__init__] Missing Mandatory Tag: @PRE<br>[AuthenticationError.__init__] Missing Mandatory Tag: @POST<br>[AuthenticationError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[AuthenticationError.__init__] Missing Mandatory Tag: @PRE<br>[AuthenticationError.__init__] Missing Mandatory Tag: @POST<br>[AuthenticationError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[PermissionDeniedError.__init__] Missing Mandatory Tag: @PRE<br>[PermissionDeniedError.__init__] Missing Mandatory Tag: @POST<br>[PermissionDeniedError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[PermissionDeniedError.__init__] Missing Mandatory Tag: @PRE<br>[PermissionDeniedError.__init__] Missing Mandatory Tag: @POST<br>[PermissionDeniedError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[PermissionDeniedError.__init__] Missing Mandatory Tag: @PRE<br>[PermissionDeniedError.__init__] Missing Mandatory Tag: @POST<br>[PermissionDeniedError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[DashboardNotFoundError.__init__] Missing Mandatory Tag: @PRE<br>[DashboardNotFoundError.__init__] Missing Mandatory Tag: @POST<br>[DashboardNotFoundError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[DashboardNotFoundError.__init__] Missing Mandatory Tag: @PRE<br>[DashboardNotFoundError.__init__] Missing Mandatory Tag: @POST<br>[DashboardNotFoundError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[DashboardNotFoundError.__init__] Missing Mandatory Tag: @PRE<br>[DashboardNotFoundError.__init__] Missing Mandatory Tag: @POST<br>[DashboardNotFoundError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[NetworkError.__init__] Missing Mandatory Tag: @PRE<br>[NetworkError.__init__] Missing Mandatory Tag: @POST<br>[NetworkError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[NetworkError.__init__] Missing Mandatory Tag: @PRE<br>[NetworkError.__init__] Missing Mandatory Tag: @POST<br>[NetworkError.__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[NetworkError.__init__] Missing Mandatory Tag: @PRE<br>[NetworkError.__init__] Missing Mandatory Tag: @POST<br>[NetworkError.__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/logger.py | 🟡 93% | [format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[belief_scope] Missing Belief State Logging: Function should use belief_scope context manager.<br>[belief_scope] Missing Belief State Logging: Function should use belief_scope context manager.<br>[configure_logger] Missing Belief State Logging: Function should use belief_scope context manager.<br>[configure_logger] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST |
| frontend/src/components/git/CommitModal.svelte | 🟡 94% | [loadStatus] Missing Mandatory Tag: @POST<br>[loadStatus] Missing Mandatory Tag: @POST |
| frontend/src/components/DashboardGrid.svelte | 🟡 94% | [openGit] Missing Mandatory Tag: @PRE<br>[openGit] Missing Mandatory Tag: @POST<br>[openGit] Missing Mandatory Tag: @PRE<br>[openGit] Missing Mandatory Tag: @POST |
| frontend/src/components/git/DeploymentModal.svelte | 🟡 96% | [loadEnvironments] Missing Mandatory Tag: @PRE<br>[loadEnvironments] Missing Mandatory Tag: @PRE |
| backend/src/core/utils/dataset_mapper.py | 🟡 97% | [__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| generate_semantic_map.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__enter__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__enter__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__exit__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__exit__] Missing Belief State Logging: Function should use belief_scope context manager. |
| frontend/src/components/TaskHistory.svelte | 🟢 100% | OK |
| frontend/src/components/MappingTable.svelte | 🟢 100% | OK |
| frontend/src/components/EnvSelector.svelte | 🟢 100% | OK |
| frontend/src/components/TaskList.svelte | 🟢 100% | OK |
| frontend/src/components/DynamicForm.svelte | 🟢 100% | OK |
| frontend/src/components/Footer.svelte | 🟢 100% | OK |
| frontend/src/components/Navbar.svelte | 🟢 100% | OK |
| frontend/src/components/TaskRunner.svelte | 🟢 100% | OK |
| frontend/src/components/TaskLogViewer.svelte | 🟢 100% | OK |
| frontend/src/components/PasswordPrompt.svelte | 🟢 100% | OK |
| frontend/src/components/MissingMappingModal.svelte | 🟢 100% | OK |
| frontend/src/components/Toast.svelte | 🟢 100% | OK |
| frontend/src/components/git/ConflictResolver.svelte | 🟢 100% | OK |
| frontend/src/components/tools/DebugTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/ConnectionForm.svelte | 🟢 100% | OK |
| frontend/src/components/tools/MapperTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/SearchTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/ConnectionList.svelte | 🟢 100% | OK |
| frontend/src/pages/Settings.svelte | 🟢 100% | OK |
| frontend/src/pages/Dashboard.svelte | 🟢 100% | OK |
| frontend/src/services/gitService.js | 🟢 100% | OK |
| frontend/src/services/connectionService.js | 🟢 100% | OK |
| frontend/src/services/taskService.js | 🟢 100% | OK |
| frontend/src/services/toolsService.js | 🟢 100% | OK |
| frontend/src/lib/stores.js | 🟢 100% | OK |
| frontend/src/lib/toasts.js | 🟢 100% | OK |
| frontend/src/lib/api.js | 🟢 100% | OK |
| frontend/src/routes/+page.ts | 🟢 100% | OK |
| frontend/src/routes/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tasks/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/+page.ts | 🟢 100% | OK |
| frontend/src/routes/settings/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/connections/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/mapper/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/debug/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/search/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/migration/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/migration/mappings/+page.svelte | 🟢 100% | OK |
| backend/delete_running_tasks.py | 🟢 100% | [delete_running_tasks] Missing Belief State Logging: Function should use belief_scope context manager.<br>[delete_running_tasks] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/dependencies.py | 🟢 100% | OK |
| backend/src/app.py | 🟢 100% | OK |
| backend/src/models/mapping.py | 🟢 100% | OK |
| backend/src/models/dashboard.py | 🟢 100% | OK |
| backend/src/models/task.py | 🟢 100% | OK |
| backend/src/models/connection.py | 🟢 100% | OK |
| backend/src/services/mapping_service.py | 🟢 100% | OK |
| backend/src/core/config_manager.py | 🟢 100% | OK |
| backend/src/core/superset_client.py | 🟢 100% | OK |
| backend/src/core/migration_engine.py | 🟢 100% | [_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/database.py | 🟢 100% | OK |
| backend/src/core/config_models.py | 🟢 100% | OK |
| backend/src/core/scheduler.py | 🟢 100% | OK |
| backend/src/core/plugin_loader.py | 🟢 100% | OK |
| backend/src/core/plugin_base.py | 🟢 100% | OK |
| backend/src/core/utils/matching.py | 🟢 100% | [suggest_mappings] Missing Belief State Logging: Function should use belief_scope context manager.<br>[suggest_mappings] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/utils/fileio.py | 🟢 100% | [replacer] Missing Belief State Logging: Function should use belief_scope context manager.<br>[replacer] Missing Belief State Logging: Function should use belief_scope context manager.<br>[replacer] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/manager.py | 🟢 100% | OK |
| backend/src/core/task_manager/__init__.py | 🟢 100% | OK |
| backend/src/core/task_manager/cleanup.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/models.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/persistence.py | 🟢 100% | OK |
| backend/src/plugins/search.py | 🟢 100% | OK |
| backend/src/plugins/backup.py | 🟢 100% | OK |
| backend/src/plugins/debug.py | 🟢 100% | OK |
| backend/src/plugins/migration.py | 🟢 100% | OK |
| backend/src/plugins/mapper.py | 🟢 100% | OK |
| backend/src/api/auth.py | 🟢 100% | [get_current_user] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_current_user] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/api/routes/settings.py | 🟢 100% | OK |
| backend/src/api/routes/connections.py | 🟢 100% | OK |
| backend/src/api/routes/tasks.py | 🟢 100% | OK |
| backend/src/api/routes/environments.py | 🟢 100% | OK |
| backend/src/api/routes/plugins.py | 🟢 100% | OK |
| backend/src/api/routes/migration.py | 🟢 100% | OK |
| backend/src/api/routes/mappings.py | 🟢 100% | OK |
| backend/tests/test_models.py | 🟢 100% | OK |
| backend/tests/test_logger.py | 🟢 100% | OK |

View File

@@ -0,0 +1,102 @@
# Semantic Compliance Report
**Generated At:** 2026-01-23T21:53:13.473176
**Global Compliance Score:** 98.0%
**Scanned Files:** 93
## File Compliance Status
| File | Score | Issues |
|------|-------|--------|
| frontend/src/components/git/GitManager.svelte | 🟡 67% | [checkStatus] Missing Mandatory Tag: @PRE<br>[checkStatus] Missing Mandatory Tag: @POST<br>[checkStatus] Missing Mandatory Tag: @PRE<br>[checkStatus] Missing Mandatory Tag: @POST<br>[handleInit] Missing Mandatory Tag: @PRE<br>[handleInit] Missing Mandatory Tag: @POST<br>[handleInit] Missing Mandatory Tag: @PRE<br>[handleInit] Missing Mandatory Tag: @POST<br>[handleSync] Missing Mandatory Tag: @PRE<br>[handleSync] Missing Mandatory Tag: @POST<br>[handleSync] Missing Mandatory Tag: @PRE<br>[handleSync] Missing Mandatory Tag: @POST<br>[handlePush] Missing Mandatory Tag: @PURPOSE<br>[handlePush] Missing Mandatory Tag: @PRE<br>[handlePush] Missing Mandatory Tag: @POST<br>[handlePush] Missing Mandatory Tag: @PURPOSE<br>[handlePush] Missing Mandatory Tag: @PRE<br>[handlePush] Missing Mandatory Tag: @POST<br>[handlePull] Missing Mandatory Tag: @PURPOSE<br>[handlePull] Missing Mandatory Tag: @PRE<br>[handlePull] Missing Mandatory Tag: @POST<br>[handlePull] Missing Mandatory Tag: @PURPOSE<br>[handlePull] Missing Mandatory Tag: @PRE<br>[handlePull] Missing Mandatory Tag: @POST |
| backend/src/services/git_service.py | 🟡 72% | [__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Mandatory Tag: @POST<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Mandatory Tag: @POST<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Mandatory Tag: @POST<br>[_get_repo_path] Missing Mandatory Tag: @PRE<br>[_get_repo_path] Missing Mandatory Tag: @POST<br>[_get_repo_path] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_get_repo_path] Missing Mandatory Tag: @PRE<br>[_get_repo_path] Missing Mandatory Tag: @POST<br>[_get_repo_path] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_get_repo_path] Missing Mandatory Tag: @PRE<br>[_get_repo_path] Missing Mandatory Tag: @POST<br>[_get_repo_path] Missing Belief State Logging: Function should use belief_scope context manager.<br>[init_repo] Missing Mandatory Tag: @PRE<br>[init_repo] Missing Mandatory Tag: @POST<br>[init_repo] Missing Mandatory Tag: @PRE<br>[init_repo] Missing Mandatory Tag: @POST<br>[init_repo] Missing Mandatory Tag: @PRE<br>[init_repo] Missing Mandatory Tag: @POST<br>[get_repo] Missing Mandatory Tag: @POST<br>[get_repo] Missing Mandatory Tag: @POST<br>[get_repo] Missing Mandatory Tag: @POST<br>[list_branches] Missing Mandatory Tag: @PRE<br>[list_branches] Missing Mandatory Tag: @POST<br>[list_branches] Missing Mandatory Tag: @PRE<br>[list_branches] Missing Mandatory Tag: @POST<br>[list_branches] Missing Mandatory Tag: @PRE<br>[list_branches] Missing Mandatory Tag: @POST<br>[create_branch] Missing Mandatory Tag: @PRE<br>[create_branch] Missing Mandatory Tag: @POST<br>[create_branch] Missing Mandatory Tag: @PRE<br>[create_branch] Missing Mandatory Tag: @POST<br>[create_branch] Missing Mandatory Tag: @PRE<br>[create_branch] Missing Mandatory Tag: @POST<br>[checkout_branch] Missing Mandatory Tag: @PRE<br>[checkout_branch] Missing Mandatory Tag: @POST<br>[checkout_branch] Missing Mandatory Tag: @PRE<br>[checkout_branch] Missing Mandatory Tag: @POST<br>[checkout_branch] Missing Mandatory Tag: @PRE<br>[checkout_branch] Missing Mandatory Tag: @POST<br>[commit_changes] Missing Mandatory Tag: @PRE<br>[commit_changes] Missing Mandatory Tag: @POST<br>[commit_changes] Missing Mandatory Tag: @PRE<br>[commit_changes] Missing Mandatory Tag: @POST<br>[commit_changes] Missing Mandatory Tag: @PRE<br>[commit_changes] Missing Mandatory Tag: @POST<br>[push_changes] Missing Mandatory Tag: @PRE<br>[push_changes] Missing Mandatory Tag: @POST<br>[push_changes] Missing Mandatory Tag: @PRE<br>[push_changes] Missing Mandatory Tag: @POST<br>[push_changes] Missing Mandatory Tag: @PRE<br>[push_changes] Missing Mandatory Tag: @POST<br>[pull_changes] Missing Mandatory Tag: @PRE<br>[pull_changes] Missing Mandatory Tag: @POST<br>[pull_changes] Missing Mandatory Tag: @PRE<br>[pull_changes] Missing Mandatory Tag: @POST<br>[pull_changes] Missing Mandatory Tag: @PRE<br>[pull_changes] Missing Mandatory Tag: @POST<br>[get_status] Missing Mandatory Tag: @PRE<br>[get_status] Missing Mandatory Tag: @POST<br>[get_status] Missing Mandatory Tag: @PRE<br>[get_status] Missing Mandatory Tag: @POST<br>[get_status] Missing Mandatory Tag: @PRE<br>[get_status] Missing Mandatory Tag: @POST<br>[get_diff] Missing Mandatory Tag: @PRE<br>[get_diff] Missing Mandatory Tag: @POST<br>[get_diff] Missing Mandatory Tag: @PRE<br>[get_diff] Missing Mandatory Tag: @POST<br>[get_diff] Missing Mandatory Tag: @PRE<br>[get_diff] Missing Mandatory Tag: @POST<br>[get_commit_history] Missing Mandatory Tag: @PRE<br>[get_commit_history] Missing Mandatory Tag: @POST<br>[get_commit_history] Missing Mandatory Tag: @PRE<br>[get_commit_history] Missing Mandatory Tag: @POST<br>[get_commit_history] Missing Mandatory Tag: @PRE<br>[get_commit_history] Missing Mandatory Tag: @POST<br>[test_connection] Missing Mandatory Tag: @PRE<br>[test_connection] Missing Mandatory Tag: @POST<br>[test_connection] Missing Mandatory Tag: @PRE<br>[test_connection] Missing Mandatory Tag: @POST<br>[test_connection] Missing Mandatory Tag: @PRE<br>[test_connection] Missing Mandatory Tag: @POST |
| frontend/src/routes/settings/git/+page.svelte | 🟡 73% | [loadConfigs] Missing Mandatory Tag: @PRE<br>[loadConfigs] Missing Mandatory Tag: @POST<br>[loadConfigs] Missing Mandatory Tag: @PRE<br>[loadConfigs] Missing Mandatory Tag: @POST<br>[handleTest] Missing Mandatory Tag: @PRE<br>[handleTest] Missing Mandatory Tag: @POST<br>[handleTest] Missing Mandatory Tag: @PRE<br>[handleTest] Missing Mandatory Tag: @POST<br>[handleSave] Missing Mandatory Tag: @PRE<br>[handleSave] Missing Mandatory Tag: @POST<br>[handleSave] Missing Mandatory Tag: @PRE<br>[handleSave] Missing Mandatory Tag: @POST<br>[handleDelete] Missing Mandatory Tag: @PRE<br>[handleDelete] Missing Mandatory Tag: @POST<br>[handleDelete] Missing Mandatory Tag: @PRE<br>[handleDelete] Missing Mandatory Tag: @POST |
| frontend/src/components/git/BranchSelector.svelte | 🟡 75% | [onMount] Missing Mandatory Tag: @PURPOSE<br>[onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[onMount] Missing Mandatory Tag: @PURPOSE<br>[onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[loadBranches] Missing Mandatory Tag: @PRE<br>[loadBranches] Missing Mandatory Tag: @PRE<br>[handleSelect] Missing Mandatory Tag: @PURPOSE<br>[handleSelect] Missing Mandatory Tag: @PRE<br>[handleSelect] Missing Mandatory Tag: @POST<br>[handleSelect] Missing Mandatory Tag: @PURPOSE<br>[handleSelect] Missing Mandatory Tag: @PRE<br>[handleSelect] Missing Mandatory Tag: @POST<br>[handleCheckout] Missing Mandatory Tag: @PRE<br>[handleCheckout] Missing Mandatory Tag: @PRE<br>[handleCreate] Missing Mandatory Tag: @PRE<br>[handleCreate] Missing Mandatory Tag: @PRE |
| backend/src/plugins/git_plugin.py | 🟡 82% | [__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Mandatory Tag: @PRE<br>[id] Missing Mandatory Tag: @PRE<br>[id] Missing Mandatory Tag: @POST<br>[id] Missing Belief State Logging: Function should use belief_scope context manager.<br>[id] Missing Mandatory Tag: @PRE<br>[id] Missing Mandatory Tag: @POST<br>[id] Missing Belief State Logging: Function should use belief_scope context manager.<br>[id] Missing Mandatory Tag: @PRE<br>[id] Missing Mandatory Tag: @POST<br>[id] Missing Belief State Logging: Function should use belief_scope context manager.<br>[name] Missing Mandatory Tag: @PRE<br>[name] Missing Mandatory Tag: @POST<br>[name] Missing Belief State Logging: Function should use belief_scope context manager.<br>[name] Missing Mandatory Tag: @PRE<br>[name] Missing Mandatory Tag: @POST<br>[name] Missing Belief State Logging: Function should use belief_scope context manager.<br>[name] Missing Mandatory Tag: @PRE<br>[name] Missing Mandatory Tag: @POST<br>[name] Missing Belief State Logging: Function should use belief_scope context manager.<br>[description] Missing Mandatory Tag: @PRE<br>[description] Missing Mandatory Tag: @POST<br>[description] Missing Belief State Logging: Function should use belief_scope context manager.<br>[description] Missing Mandatory Tag: @PRE<br>[description] Missing Mandatory Tag: @POST<br>[description] Missing Belief State Logging: Function should use belief_scope context manager.<br>[description] Missing Mandatory Tag: @PRE<br>[description] Missing Mandatory Tag: @POST<br>[description] Missing Belief State Logging: Function should use belief_scope context manager.<br>[version] Missing Mandatory Tag: @PRE<br>[version] Missing Mandatory Tag: @POST<br>[version] Missing Belief State Logging: Function should use belief_scope context manager.<br>[version] Missing Mandatory Tag: @PRE<br>[version] Missing Mandatory Tag: @POST<br>[version] Missing Belief State Logging: Function should use belief_scope context manager.<br>[version] Missing Mandatory Tag: @PRE<br>[version] Missing Mandatory Tag: @POST<br>[version] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_schema] Missing Mandatory Tag: @PRE<br>[get_schema] Missing Mandatory Tag: @POST<br>[get_schema] Missing Mandatory Tag: @PRE<br>[get_schema] Missing Mandatory Tag: @POST<br>[get_schema] Missing Mandatory Tag: @PRE<br>[get_schema] Missing Mandatory Tag: @POST<br>[initialize] Missing Mandatory Tag: @PRE<br>[initialize] Missing Mandatory Tag: @PRE<br>[initialize] Missing Mandatory Tag: @PRE<br>[_get_env] Missing Mandatory Tag: @PRE<br>[_get_env] Missing Mandatory Tag: @POST<br>[_get_env] Missing Mandatory Tag: @PRE<br>[_get_env] Missing Mandatory Tag: @POST<br>[_get_env] Missing Mandatory Tag: @PRE<br>[_get_env] Missing Mandatory Tag: @POST<br>[_get_env] Missing Mandatory Tag: @PRE<br>[_get_env] Missing Mandatory Tag: @POST |
| backend/src/models/git.py | 🟡 83% | [GitModels] Missing Mandatory Tag: @SEMANTICS |
| frontend/src/components/git/CommitHistory.svelte | 🟡 83% | [onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[loadHistory] Missing Mandatory Tag: @PRE<br>[loadHistory] Missing Mandatory Tag: @PRE |
| backend/src/core/logger.py | 🟡 93% | [format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[belief_scope] Missing Belief State Logging: Function should use belief_scope context manager.<br>[belief_scope] Missing Belief State Logging: Function should use belief_scope context manager.<br>[configure_logger] Missing Belief State Logging: Function should use belief_scope context manager.<br>[configure_logger] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST |
| frontend/src/components/git/CommitModal.svelte | 🟡 94% | [loadStatus] Missing Mandatory Tag: @POST<br>[loadStatus] Missing Mandatory Tag: @POST |
| frontend/src/components/DashboardGrid.svelte | 🟡 94% | [openGit] Missing Mandatory Tag: @PRE<br>[openGit] Missing Mandatory Tag: @POST<br>[openGit] Missing Mandatory Tag: @PRE<br>[openGit] Missing Mandatory Tag: @POST |
| frontend/src/components/git/DeploymentModal.svelte | 🟡 96% | [loadEnvironments] Missing Mandatory Tag: @PRE<br>[loadEnvironments] Missing Mandatory Tag: @PRE |
| backend/src/core/utils/dataset_mapper.py | 🟡 97% | [__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| generate_semantic_map.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__enter__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__enter__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__exit__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__exit__] Missing Belief State Logging: Function should use belief_scope context manager. |
| frontend/src/components/TaskHistory.svelte | 🟢 100% | OK |
| frontend/src/components/MappingTable.svelte | 🟢 100% | OK |
| frontend/src/components/EnvSelector.svelte | 🟢 100% | OK |
| frontend/src/components/TaskList.svelte | 🟢 100% | OK |
| frontend/src/components/DynamicForm.svelte | 🟢 100% | OK |
| frontend/src/components/Footer.svelte | 🟢 100% | OK |
| frontend/src/components/Navbar.svelte | 🟢 100% | OK |
| frontend/src/components/TaskRunner.svelte | 🟢 100% | OK |
| frontend/src/components/TaskLogViewer.svelte | 🟢 100% | OK |
| frontend/src/components/PasswordPrompt.svelte | 🟢 100% | OK |
| frontend/src/components/MissingMappingModal.svelte | 🟢 100% | OK |
| frontend/src/components/Toast.svelte | 🟢 100% | OK |
| frontend/src/components/git/ConflictResolver.svelte | 🟢 100% | OK |
| frontend/src/components/tools/DebugTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/ConnectionForm.svelte | 🟢 100% | OK |
| frontend/src/components/tools/MapperTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/SearchTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/ConnectionList.svelte | 🟢 100% | OK |
| frontend/src/pages/Settings.svelte | 🟢 100% | OK |
| frontend/src/pages/Dashboard.svelte | 🟢 100% | OK |
| frontend/src/services/gitService.js | 🟢 100% | OK |
| frontend/src/services/connectionService.js | 🟢 100% | OK |
| frontend/src/services/taskService.js | 🟢 100% | OK |
| frontend/src/services/toolsService.js | 🟢 100% | OK |
| frontend/src/lib/stores.js | 🟢 100% | OK |
| frontend/src/lib/toasts.js | 🟢 100% | OK |
| frontend/src/lib/api.js | 🟢 100% | OK |
| frontend/src/routes/+page.ts | 🟢 100% | OK |
| frontend/src/routes/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tasks/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/git/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/+page.ts | 🟢 100% | OK |
| frontend/src/routes/settings/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/connections/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/mapper/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/debug/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/search/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/migration/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/migration/mappings/+page.svelte | 🟢 100% | OK |
| backend/delete_running_tasks.py | 🟢 100% | [delete_running_tasks] Missing Belief State Logging: Function should use belief_scope context manager.<br>[delete_running_tasks] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/dependencies.py | 🟢 100% | OK |
| backend/src/app.py | 🟢 100% | OK |
| backend/src/models/mapping.py | 🟢 100% | OK |
| backend/src/models/dashboard.py | 🟢 100% | OK |
| backend/src/models/task.py | 🟢 100% | OK |
| backend/src/models/connection.py | 🟢 100% | OK |
| backend/src/services/mapping_service.py | 🟢 100% | OK |
| backend/src/core/config_manager.py | 🟢 100% | OK |
| backend/src/core/superset_client.py | 🟢 100% | OK |
| backend/src/core/migration_engine.py | 🟢 100% | [_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/database.py | 🟢 100% | OK |
| backend/src/core/config_models.py | 🟢 100% | OK |
| backend/src/core/scheduler.py | 🟢 100% | OK |
| backend/src/core/plugin_loader.py | 🟢 100% | OK |
| backend/src/core/plugin_base.py | 🟢 100% | OK |
| backend/src/core/utils/matching.py | 🟢 100% | [suggest_mappings] Missing Belief State Logging: Function should use belief_scope context manager.<br>[suggest_mappings] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/utils/network.py | 🟢 100% | OK |
| backend/src/core/utils/fileio.py | 🟢 100% | [replacer] Missing Belief State Logging: Function should use belief_scope context manager.<br>[replacer] Missing Belief State Logging: Function should use belief_scope context manager.<br>[replacer] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/manager.py | 🟢 100% | OK |
| backend/src/core/task_manager/__init__.py | 🟢 100% | OK |
| backend/src/core/task_manager/cleanup.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/models.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/persistence.py | 🟢 100% | OK |
| backend/src/plugins/search.py | 🟢 100% | OK |
| backend/src/plugins/backup.py | 🟢 100% | OK |
| backend/src/plugins/debug.py | 🟢 100% | OK |
| backend/src/plugins/migration.py | 🟢 100% | OK |
| backend/src/plugins/mapper.py | 🟢 100% | OK |
| backend/src/api/auth.py | 🟢 100% | [get_current_user] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_current_user] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/api/routes/git_schemas.py | 🟢 100% | OK |
| backend/src/api/routes/settings.py | 🟢 100% | OK |
| backend/src/api/routes/connections.py | 🟢 100% | OK |
| backend/src/api/routes/tasks.py | 🟢 100% | OK |
| backend/src/api/routes/git.py | 🟢 100% | OK |
| backend/src/api/routes/environments.py | 🟢 100% | OK |
| backend/src/api/routes/plugins.py | 🟢 100% | OK |
| backend/src/api/routes/migration.py | 🟢 100% | OK |
| backend/src/api/routes/mappings.py | 🟢 100% | OK |
| backend/tests/test_models.py | 🟢 100% | OK |
| backend/tests/test_logger.py | 🟢 100% | OK |

View File

@@ -0,0 +1,102 @@
# Semantic Compliance Report
**Generated At:** 2026-01-23T21:55:07.969831
**Global Compliance Score:** 98.6%
**Scanned Files:** 93
## File Compliance Status
| File | Score | Issues |
|------|-------|--------|
| frontend/src/components/git/GitManager.svelte | 🟡 67% | [checkStatus] Missing Mandatory Tag: @PRE<br>[checkStatus] Missing Mandatory Tag: @POST<br>[checkStatus] Missing Mandatory Tag: @PRE<br>[checkStatus] Missing Mandatory Tag: @POST<br>[handleInit] Missing Mandatory Tag: @PRE<br>[handleInit] Missing Mandatory Tag: @POST<br>[handleInit] Missing Mandatory Tag: @PRE<br>[handleInit] Missing Mandatory Tag: @POST<br>[handleSync] Missing Mandatory Tag: @PRE<br>[handleSync] Missing Mandatory Tag: @POST<br>[handleSync] Missing Mandatory Tag: @PRE<br>[handleSync] Missing Mandatory Tag: @POST<br>[handlePush] Missing Mandatory Tag: @PURPOSE<br>[handlePush] Missing Mandatory Tag: @PRE<br>[handlePush] Missing Mandatory Tag: @POST<br>[handlePush] Missing Mandatory Tag: @PURPOSE<br>[handlePush] Missing Mandatory Tag: @PRE<br>[handlePush] Missing Mandatory Tag: @POST<br>[handlePull] Missing Mandatory Tag: @PURPOSE<br>[handlePull] Missing Mandatory Tag: @PRE<br>[handlePull] Missing Mandatory Tag: @POST<br>[handlePull] Missing Mandatory Tag: @PURPOSE<br>[handlePull] Missing Mandatory Tag: @PRE<br>[handlePull] Missing Mandatory Tag: @POST |
| frontend/src/routes/settings/git/+page.svelte | 🟡 73% | [loadConfigs] Missing Mandatory Tag: @PRE<br>[loadConfigs] Missing Mandatory Tag: @POST<br>[loadConfigs] Missing Mandatory Tag: @PRE<br>[loadConfigs] Missing Mandatory Tag: @POST<br>[handleTest] Missing Mandatory Tag: @PRE<br>[handleTest] Missing Mandatory Tag: @POST<br>[handleTest] Missing Mandatory Tag: @PRE<br>[handleTest] Missing Mandatory Tag: @POST<br>[handleSave] Missing Mandatory Tag: @PRE<br>[handleSave] Missing Mandatory Tag: @POST<br>[handleSave] Missing Mandatory Tag: @PRE<br>[handleSave] Missing Mandatory Tag: @POST<br>[handleDelete] Missing Mandatory Tag: @PRE<br>[handleDelete] Missing Mandatory Tag: @POST<br>[handleDelete] Missing Mandatory Tag: @PRE<br>[handleDelete] Missing Mandatory Tag: @POST |
| frontend/src/components/git/BranchSelector.svelte | 🟡 75% | [onMount] Missing Mandatory Tag: @PURPOSE<br>[onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[onMount] Missing Mandatory Tag: @PURPOSE<br>[onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[loadBranches] Missing Mandatory Tag: @PRE<br>[loadBranches] Missing Mandatory Tag: @PRE<br>[handleSelect] Missing Mandatory Tag: @PURPOSE<br>[handleSelect] Missing Mandatory Tag: @PRE<br>[handleSelect] Missing Mandatory Tag: @POST<br>[handleSelect] Missing Mandatory Tag: @PURPOSE<br>[handleSelect] Missing Mandatory Tag: @PRE<br>[handleSelect] Missing Mandatory Tag: @POST<br>[handleCheckout] Missing Mandatory Tag: @PRE<br>[handleCheckout] Missing Mandatory Tag: @PRE<br>[handleCreate] Missing Mandatory Tag: @PRE<br>[handleCreate] Missing Mandatory Tag: @PRE |
| frontend/src/components/git/CommitHistory.svelte | 🟡 83% | [onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[onMount] Missing Mandatory Tag: @PRE<br>[onMount] Missing Mandatory Tag: @POST<br>[loadHistory] Missing Mandatory Tag: @PRE<br>[loadHistory] Missing Mandatory Tag: @PRE |
| backend/src/core/logger.py | 🟡 93% | [format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[belief_scope] Missing Belief State Logging: Function should use belief_scope context manager.<br>[belief_scope] Missing Belief State Logging: Function should use belief_scope context manager.<br>[configure_logger] Missing Belief State Logging: Function should use belief_scope context manager.<br>[configure_logger] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST |
| frontend/src/components/git/CommitModal.svelte | 🟡 94% | [loadStatus] Missing Mandatory Tag: @POST<br>[loadStatus] Missing Mandatory Tag: @POST |
| frontend/src/components/DashboardGrid.svelte | 🟡 94% | [openGit] Missing Mandatory Tag: @PRE<br>[openGit] Missing Mandatory Tag: @POST<br>[openGit] Missing Mandatory Tag: @PRE<br>[openGit] Missing Mandatory Tag: @POST |
| frontend/src/components/git/DeploymentModal.svelte | 🟡 96% | [loadEnvironments] Missing Mandatory Tag: @PRE<br>[loadEnvironments] Missing Mandatory Tag: @PRE |
| backend/src/core/utils/dataset_mapper.py | 🟡 97% | [__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| generate_semantic_map.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__enter__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__enter__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__exit__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__exit__] Missing Belief State Logging: Function should use belief_scope context manager. |
| frontend/src/components/TaskHistory.svelte | 🟢 100% | OK |
| frontend/src/components/MappingTable.svelte | 🟢 100% | OK |
| frontend/src/components/EnvSelector.svelte | 🟢 100% | OK |
| frontend/src/components/TaskList.svelte | 🟢 100% | OK |
| frontend/src/components/DynamicForm.svelte | 🟢 100% | OK |
| frontend/src/components/Footer.svelte | 🟢 100% | OK |
| frontend/src/components/Navbar.svelte | 🟢 100% | OK |
| frontend/src/components/TaskRunner.svelte | 🟢 100% | OK |
| frontend/src/components/TaskLogViewer.svelte | 🟢 100% | OK |
| frontend/src/components/PasswordPrompt.svelte | 🟢 100% | OK |
| frontend/src/components/MissingMappingModal.svelte | 🟢 100% | OK |
| frontend/src/components/Toast.svelte | 🟢 100% | OK |
| frontend/src/components/git/ConflictResolver.svelte | 🟢 100% | OK |
| frontend/src/components/tools/DebugTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/ConnectionForm.svelte | 🟢 100% | OK |
| frontend/src/components/tools/MapperTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/SearchTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/ConnectionList.svelte | 🟢 100% | OK |
| frontend/src/pages/Settings.svelte | 🟢 100% | OK |
| frontend/src/pages/Dashboard.svelte | 🟢 100% | OK |
| frontend/src/services/gitService.js | 🟢 100% | OK |
| frontend/src/services/connectionService.js | 🟢 100% | OK |
| frontend/src/services/taskService.js | 🟢 100% | OK |
| frontend/src/services/toolsService.js | 🟢 100% | OK |
| frontend/src/lib/stores.js | 🟢 100% | OK |
| frontend/src/lib/toasts.js | 🟢 100% | OK |
| frontend/src/lib/api.js | 🟢 100% | OK |
| frontend/src/routes/+page.ts | 🟢 100% | OK |
| frontend/src/routes/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tasks/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/git/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/+page.ts | 🟢 100% | OK |
| frontend/src/routes/settings/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/connections/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/mapper/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/debug/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/search/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/migration/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/migration/mappings/+page.svelte | 🟢 100% | OK |
| backend/delete_running_tasks.py | 🟢 100% | [delete_running_tasks] Missing Belief State Logging: Function should use belief_scope context manager.<br>[delete_running_tasks] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/dependencies.py | 🟢 100% | OK |
| backend/src/app.py | 🟢 100% | OK |
| backend/src/models/mapping.py | 🟢 100% | OK |
| backend/src/models/git.py | 🟢 100% | OK |
| backend/src/models/dashboard.py | 🟢 100% | OK |
| backend/src/models/task.py | 🟢 100% | OK |
| backend/src/models/connection.py | 🟢 100% | OK |
| backend/src/services/mapping_service.py | 🟢 100% | OK |
| backend/src/services/git_service.py | 🟢 100% | OK |
| backend/src/core/config_manager.py | 🟢 100% | OK |
| backend/src/core/superset_client.py | 🟢 100% | OK |
| backend/src/core/migration_engine.py | 🟢 100% | [_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/database.py | 🟢 100% | OK |
| backend/src/core/config_models.py | 🟢 100% | OK |
| backend/src/core/scheduler.py | 🟢 100% | OK |
| backend/src/core/plugin_loader.py | 🟢 100% | OK |
| backend/src/core/plugin_base.py | 🟢 100% | OK |
| backend/src/core/utils/matching.py | 🟢 100% | [suggest_mappings] Missing Belief State Logging: Function should use belief_scope context manager.<br>[suggest_mappings] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/utils/network.py | 🟢 100% | OK |
| backend/src/core/utils/fileio.py | 🟢 100% | [replacer] Missing Belief State Logging: Function should use belief_scope context manager.<br>[replacer] Missing Belief State Logging: Function should use belief_scope context manager.<br>[replacer] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/manager.py | 🟢 100% | OK |
| backend/src/core/task_manager/__init__.py | 🟢 100% | OK |
| backend/src/core/task_manager/cleanup.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/models.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/persistence.py | 🟢 100% | OK |
| backend/src/plugins/search.py | 🟢 100% | OK |
| backend/src/plugins/backup.py | 🟢 100% | OK |
| backend/src/plugins/debug.py | 🟢 100% | OK |
| backend/src/plugins/migration.py | 🟢 100% | OK |
| backend/src/plugins/git_plugin.py | 🟢 100% | OK |
| backend/src/plugins/mapper.py | 🟢 100% | OK |
| backend/src/api/auth.py | 🟢 100% | [get_current_user] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_current_user] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/api/routes/git_schemas.py | 🟢 100% | OK |
| backend/src/api/routes/settings.py | 🟢 100% | OK |
| backend/src/api/routes/connections.py | 🟢 100% | OK |
| backend/src/api/routes/tasks.py | 🟢 100% | OK |
| backend/src/api/routes/git.py | 🟢 100% | OK |
| backend/src/api/routes/environments.py | 🟢 100% | OK |
| backend/src/api/routes/plugins.py | 🟢 100% | OK |
| backend/src/api/routes/migration.py | 🟢 100% | OK |
| backend/src/api/routes/mappings.py | 🟢 100% | OK |
| backend/tests/test_models.py | 🟢 100% | OK |
| backend/tests/test_logger.py | 🟢 100% | OK |

View File

@@ -0,0 +1,102 @@
# Semantic Compliance Report
**Generated At:** 2026-01-23T21:57:37.610032
**Global Compliance Score:** 99.7%
**Scanned Files:** 93
## File Compliance Status
| File | Score | Issues |
|------|-------|--------|
| backend/src/core/logger.py | 🟡 93% | [format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[format] Missing Belief State Logging: Function should use belief_scope context manager.<br>[belief_scope] Missing Belief State Logging: Function should use belief_scope context manager.<br>[belief_scope] Missing Belief State Logging: Function should use belief_scope context manager.<br>[configure_logger] Missing Belief State Logging: Function should use belief_scope context manager.<br>[configure_logger] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[emit] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_recent_logs] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[believed] Missing Mandatory Tag: @PRE<br>[believed] Missing Mandatory Tag: @POST<br>[believed] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[decorator] Missing Mandatory Tag: @PRE<br>[decorator] Missing Mandatory Tag: @POST<br>[decorator] Missing Belief State Logging: Function should use belief_scope context manager.<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST<br>[wrapper] Missing Mandatory Tag: @PRE<br>[wrapper] Missing Mandatory Tag: @POST |
| frontend/src/components/git/CommitModal.svelte | 🟡 94% | [loadStatus] Missing Mandatory Tag: @POST<br>[loadStatus] Missing Mandatory Tag: @POST |
| frontend/src/components/DashboardGrid.svelte | 🟡 94% | [openGit] Missing Mandatory Tag: @PRE<br>[openGit] Missing Mandatory Tag: @POST<br>[openGit] Missing Mandatory Tag: @PRE<br>[openGit] Missing Mandatory Tag: @POST |
| frontend/src/components/git/DeploymentModal.svelte | 🟡 96% | [loadEnvironments] Missing Mandatory Tag: @PRE<br>[loadEnvironments] Missing Mandatory Tag: @PRE |
| frontend/src/components/git/BranchSelector.svelte | 🟡 97% | [handleCheckout] Missing Mandatory Tag: @PRE<br>[handleCheckout] Missing Mandatory Tag: @PRE |
| backend/src/core/utils/dataset_mapper.py | 🟡 97% | [__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| generate_semantic_map.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__enter__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__enter__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__exit__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__exit__] Missing Belief State Logging: Function should use belief_scope context manager. |
| frontend/src/components/TaskHistory.svelte | 🟢 100% | OK |
| frontend/src/components/MappingTable.svelte | 🟢 100% | OK |
| frontend/src/components/EnvSelector.svelte | 🟢 100% | OK |
| frontend/src/components/TaskList.svelte | 🟢 100% | OK |
| frontend/src/components/DynamicForm.svelte | 🟢 100% | OK |
| frontend/src/components/Footer.svelte | 🟢 100% | OK |
| frontend/src/components/Navbar.svelte | 🟢 100% | OK |
| frontend/src/components/TaskRunner.svelte | 🟢 100% | OK |
| frontend/src/components/TaskLogViewer.svelte | 🟢 100% | OK |
| frontend/src/components/PasswordPrompt.svelte | 🟢 100% | OK |
| frontend/src/components/MissingMappingModal.svelte | 🟢 100% | OK |
| frontend/src/components/Toast.svelte | 🟢 100% | OK |
| frontend/src/components/git/GitManager.svelte | 🟢 100% | OK |
| frontend/src/components/git/ConflictResolver.svelte | 🟢 100% | OK |
| frontend/src/components/git/CommitHistory.svelte | 🟢 100% | OK |
| frontend/src/components/tools/DebugTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/ConnectionForm.svelte | 🟢 100% | OK |
| frontend/src/components/tools/MapperTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/SearchTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/ConnectionList.svelte | 🟢 100% | OK |
| frontend/src/pages/Settings.svelte | 🟢 100% | OK |
| frontend/src/pages/Dashboard.svelte | 🟢 100% | OK |
| frontend/src/services/gitService.js | 🟢 100% | OK |
| frontend/src/services/connectionService.js | 🟢 100% | OK |
| frontend/src/services/taskService.js | 🟢 100% | OK |
| frontend/src/services/toolsService.js | 🟢 100% | OK |
| frontend/src/lib/stores.js | 🟢 100% | OK |
| frontend/src/lib/toasts.js | 🟢 100% | OK |
| frontend/src/lib/api.js | 🟢 100% | OK |
| frontend/src/routes/+page.ts | 🟢 100% | OK |
| frontend/src/routes/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tasks/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/git/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/+page.ts | 🟢 100% | OK |
| frontend/src/routes/settings/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/git/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/connections/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/mapper/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/debug/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/search/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/migration/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/migration/mappings/+page.svelte | 🟢 100% | OK |
| backend/delete_running_tasks.py | 🟢 100% | [delete_running_tasks] Missing Belief State Logging: Function should use belief_scope context manager.<br>[delete_running_tasks] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/dependencies.py | 🟢 100% | OK |
| backend/src/app.py | 🟢 100% | OK |
| backend/src/models/mapping.py | 🟢 100% | OK |
| backend/src/models/git.py | 🟢 100% | OK |
| backend/src/models/dashboard.py | 🟢 100% | OK |
| backend/src/models/task.py | 🟢 100% | OK |
| backend/src/models/connection.py | 🟢 100% | OK |
| backend/src/services/mapping_service.py | 🟢 100% | OK |
| backend/src/services/git_service.py | 🟢 100% | OK |
| backend/src/core/config_manager.py | 🟢 100% | OK |
| backend/src/core/superset_client.py | 🟢 100% | OK |
| backend/src/core/migration_engine.py | 🟢 100% | [_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/database.py | 🟢 100% | OK |
| backend/src/core/config_models.py | 🟢 100% | OK |
| backend/src/core/scheduler.py | 🟢 100% | OK |
| backend/src/core/plugin_loader.py | 🟢 100% | OK |
| backend/src/core/plugin_base.py | 🟢 100% | OK |
| backend/src/core/utils/matching.py | 🟢 100% | [suggest_mappings] Missing Belief State Logging: Function should use belief_scope context manager.<br>[suggest_mappings] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/utils/network.py | 🟢 100% | OK |
| backend/src/core/utils/fileio.py | 🟢 100% | [replacer] Missing Belief State Logging: Function should use belief_scope context manager.<br>[replacer] Missing Belief State Logging: Function should use belief_scope context manager.<br>[replacer] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/manager.py | 🟢 100% | OK |
| backend/src/core/task_manager/__init__.py | 🟢 100% | OK |
| backend/src/core/task_manager/cleanup.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/models.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/persistence.py | 🟢 100% | OK |
| backend/src/plugins/search.py | 🟢 100% | OK |
| backend/src/plugins/backup.py | 🟢 100% | OK |
| backend/src/plugins/debug.py | 🟢 100% | OK |
| backend/src/plugins/migration.py | 🟢 100% | OK |
| backend/src/plugins/git_plugin.py | 🟢 100% | OK |
| backend/src/plugins/mapper.py | 🟢 100% | OK |
| backend/src/api/auth.py | 🟢 100% | [get_current_user] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_current_user] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/api/routes/git_schemas.py | 🟢 100% | OK |
| backend/src/api/routes/settings.py | 🟢 100% | OK |
| backend/src/api/routes/connections.py | 🟢 100% | OK |
| backend/src/api/routes/tasks.py | 🟢 100% | OK |
| backend/src/api/routes/git.py | 🟢 100% | OK |
| backend/src/api/routes/environments.py | 🟢 100% | OK |
| backend/src/api/routes/plugins.py | 🟢 100% | OK |
| backend/src/api/routes/migration.py | 🟢 100% | OK |
| backend/src/api/routes/mappings.py | 🟢 100% | OK |
| backend/tests/test_models.py | 🟢 100% | OK |
| backend/tests/test_logger.py | 🟢 100% | OK |

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,117 @@
# Semantic Compliance Report
**Generated At:** 2026-01-26T11:41:28.355350
**Global Compliance Score:** 99.2%
**Scanned Files:** 108
## File Compliance Status
| File | Score | Issues |
|------|-------|--------|
| frontend/src/components/storage/FileList.svelte | 🟡 75% | [isDirectory] Missing Mandatory Tag: @PRE<br>[isDirectory] Missing Mandatory Tag: @POST<br>[isDirectory] Missing Mandatory Tag: @PRE<br>[isDirectory] Missing Mandatory Tag: @POST<br>[formatSize] Missing Mandatory Tag: @PRE<br>[formatSize] Missing Mandatory Tag: @POST<br>[formatSize] Missing Mandatory Tag: @PRE<br>[formatSize] Missing Mandatory Tag: @POST<br>[formatDate] Missing Mandatory Tag: @PRE<br>[formatDate] Missing Mandatory Tag: @POST<br>[formatDate] Missing Mandatory Tag: @PRE<br>[formatDate] Missing Mandatory Tag: @POST |
| frontend/src/routes/tools/storage/+page.svelte | 🟡 77% | [loadFiles] Missing Mandatory Tag: @PRE<br>[loadFiles] Missing Mandatory Tag: @PRE<br>[handleDelete] Missing Mandatory Tag: @PRE<br>[handleDelete] Missing Mandatory Tag: @POST<br>[handleDelete] Missing Mandatory Tag: @PRE<br>[handleDelete] Missing Mandatory Tag: @POST<br>[handleNavigate] Missing Mandatory Tag: @PRE<br>[handleNavigate] Missing Mandatory Tag: @POST<br>[handleNavigate] Missing Mandatory Tag: @PRE<br>[handleNavigate] Missing Mandatory Tag: @POST<br>[navigateUp] Missing Mandatory Tag: @PRE<br>[navigateUp] Missing Mandatory Tag: @POST<br>[navigateUp] Missing Mandatory Tag: @PRE<br>[navigateUp] Missing Mandatory Tag: @POST |
| frontend/src/components/storage/FileUpload.svelte | 🟡 89% | [handleDrop] Missing Mandatory Tag: @PRE<br>[handleDrop] Missing Mandatory Tag: @POST<br>[handleDrop] Missing Mandatory Tag: @PRE<br>[handleDrop] Missing Mandatory Tag: @POST |
| frontend/src/components/git/CommitModal.svelte | 🟡 94% | [loadStatus] Missing Mandatory Tag: @POST<br>[loadStatus] Missing Mandatory Tag: @POST |
| frontend/src/components/DashboardGrid.svelte | 🟡 94% | [openGit] Missing Mandatory Tag: @PRE<br>[openGit] Missing Mandatory Tag: @POST<br>[openGit] Missing Mandatory Tag: @PRE<br>[openGit] Missing Mandatory Tag: @POST |
| backend/src/api/routes/settings.py | 🟡 95% | [get_storage_settings] Missing Mandatory Tag: @PRE<br>[get_storage_settings] Missing Mandatory Tag: @POST<br>[get_storage_settings] Missing Mandatory Tag: @PRE<br>[get_storage_settings] Missing Mandatory Tag: @POST<br>[update_storage_settings] Missing Mandatory Tag: @PRE<br>[update_storage_settings] Missing Mandatory Tag: @PRE |
| frontend/src/components/git/DeploymentModal.svelte | 🟡 96% | [loadEnvironments] Missing Mandatory Tag: @PRE<br>[loadEnvironments] Missing Mandatory Tag: @PRE |
| frontend/src/components/git/BranchSelector.svelte | 🟡 97% | [handleCheckout] Missing Mandatory Tag: @PRE<br>[handleCheckout] Missing Mandatory Tag: @PRE |
| backend/src/core/utils/dataset_mapper.py | 🟡 97% | [__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Mandatory Tag: @PRE<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| generate_semantic_map.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__enter__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__enter__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__exit__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__exit__] Missing Belief State Logging: Function should use belief_scope context manager. |
| frontend/src/lib/stores.js | 🟢 100% | OK |
| frontend/src/lib/toasts.js | 🟢 100% | OK |
| frontend/src/lib/api.js | 🟢 100% | OK |
| frontend/src/lib/ui/Select.svelte | 🟢 100% | OK |
| frontend/src/lib/ui/index.ts | 🟢 100% | OK |
| frontend/src/lib/ui/PageHeader.svelte | 🟢 100% | OK |
| frontend/src/lib/ui/Card.svelte | 🟢 100% | OK |
| frontend/src/lib/ui/Button.svelte | 🟢 100% | OK |
| frontend/src/lib/ui/Input.svelte | 🟢 100% | OK |
| frontend/src/lib/ui/LanguageSwitcher.svelte | 🟢 100% | OK |
| frontend/src/lib/i18n/index.ts | 🟢 100% | OK |
| frontend/src/routes/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/+page.ts | 🟢 100% | OK |
| frontend/src/routes/tasks/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/migration/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/migration/mappings/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/search/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/mapper/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/tools/debug/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/+page.ts | 🟢 100% | OK |
| frontend/src/routes/settings/connections/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/settings/git/+page.svelte | 🟢 100% | OK |
| frontend/src/routes/git/+page.svelte | 🟢 100% | OK |
| frontend/src/pages/Dashboard.svelte | 🟢 100% | OK |
| frontend/src/pages/Settings.svelte | 🟢 100% | OK |
| frontend/src/services/connectionService.js | 🟢 100% | OK |
| frontend/src/services/gitService.js | 🟢 100% | OK |
| frontend/src/services/toolsService.js | 🟢 100% | OK |
| frontend/src/services/taskService.js | 🟢 100% | OK |
| frontend/src/services/storageService.js | 🟢 100% | OK |
| frontend/src/components/PasswordPrompt.svelte | 🟢 100% | OK |
| frontend/src/components/MappingTable.svelte | 🟢 100% | OK |
| frontend/src/components/TaskLogViewer.svelte | 🟢 100% | OK |
| frontend/src/components/Footer.svelte | 🟢 100% | OK |
| frontend/src/components/MissingMappingModal.svelte | 🟢 100% | OK |
| frontend/src/components/Navbar.svelte | 🟢 100% | OK |
| frontend/src/components/TaskHistory.svelte | 🟢 100% | OK |
| frontend/src/components/Toast.svelte | 🟢 100% | OK |
| frontend/src/components/TaskRunner.svelte | 🟢 100% | OK |
| frontend/src/components/TaskList.svelte | 🟢 100% | OK |
| frontend/src/components/DynamicForm.svelte | 🟢 100% | OK |
| frontend/src/components/EnvSelector.svelte | 🟢 100% | OK |
| frontend/src/components/tools/ConnectionForm.svelte | 🟢 100% | OK |
| frontend/src/components/tools/ConnectionList.svelte | 🟢 100% | OK |
| frontend/src/components/tools/MapperTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/DebugTool.svelte | 🟢 100% | OK |
| frontend/src/components/tools/SearchTool.svelte | 🟢 100% | OK |
| frontend/src/components/git/CommitHistory.svelte | 🟢 100% | OK |
| frontend/src/components/git/ConflictResolver.svelte | 🟢 100% | OK |
| frontend/src/components/git/GitManager.svelte | 🟢 100% | OK |
| backend/delete_running_tasks.py | 🟢 100% | [delete_running_tasks] Missing Belief State Logging: Function should use belief_scope context manager.<br>[delete_running_tasks] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/app.py | 🟢 100% | OK |
| backend/src/dependencies.py | 🟢 100% | OK |
| backend/src/core/superset_client.py | 🟢 100% | OK |
| backend/src/core/config_manager.py | 🟢 100% | OK |
| backend/src/core/scheduler.py | 🟢 100% | OK |
| backend/src/core/config_models.py | 🟢 100% | OK |
| backend/src/core/database.py | 🟢 100% | OK |
| backend/src/core/logger.py | 🟢 100% | OK |
| backend/src/core/plugin_loader.py | 🟢 100% | OK |
| backend/src/core/migration_engine.py | 🟢 100% | [_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager.<br>[_transform_yaml] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/plugin_base.py | 🟢 100% | OK |
| backend/src/core/utils/fileio.py | 🟢 100% | [replacer] Missing Belief State Logging: Function should use belief_scope context manager.<br>[replacer] Missing Belief State Logging: Function should use belief_scope context manager.<br>[replacer] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/utils/network.py | 🟢 100% | OK |
| backend/src/core/utils/matching.py | 🟢 100% | [suggest_mappings] Missing Belief State Logging: Function should use belief_scope context manager.<br>[suggest_mappings] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/persistence.py | 🟢 100% | OK |
| backend/src/core/task_manager/manager.py | 🟢 100% | OK |
| backend/src/core/task_manager/models.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/cleanup.py | 🟢 100% | [__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager.<br>[__init__] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/core/task_manager/__init__.py | 🟢 100% | OK |
| backend/src/api/auth.py | 🟢 100% | [get_current_user] Missing Belief State Logging: Function should use belief_scope context manager.<br>[get_current_user] Missing Belief State Logging: Function should use belief_scope context manager. |
| backend/src/api/routes/git.py | 🟢 100% | OK |
| backend/src/api/routes/connections.py | 🟢 100% | OK |
| backend/src/api/routes/environments.py | 🟢 100% | OK |
| backend/src/api/routes/migration.py | 🟢 100% | OK |
| backend/src/api/routes/plugins.py | 🟢 100% | OK |
| backend/src/api/routes/mappings.py | 🟢 100% | OK |
| backend/src/api/routes/git_schemas.py | 🟢 100% | OK |
| backend/src/api/routes/storage.py | 🟢 100% | OK |
| backend/src/api/routes/tasks.py | 🟢 100% | OK |
| backend/src/models/git.py | 🟢 100% | OK |
| backend/src/models/task.py | 🟢 100% | OK |
| backend/src/models/connection.py | 🟢 100% | OK |
| backend/src/models/mapping.py | 🟢 100% | OK |
| backend/src/models/storage.py | 🟢 100% | OK |
| backend/src/models/dashboard.py | 🟢 100% | OK |
| backend/src/services/git_service.py | 🟢 100% | OK |
| backend/src/services/mapping_service.py | 🟢 100% | OK |
| backend/src/plugins/backup.py | 🟢 100% | OK |
| backend/src/plugins/debug.py | 🟢 100% | OK |
| backend/src/plugins/search.py | 🟢 100% | OK |
| backend/src/plugins/mapper.py | 🟢 100% | OK |
| backend/src/plugins/git_plugin.py | 🟢 100% | OK |
| backend/src/plugins/migration.py | 🟢 100% | OK |
| backend/src/plugins/storage/plugin.py | 🟢 100% | OK |
| backend/tests/test_models.py | 🟢 100% | OK |
| backend/tests/test_logger.py | 🟢 100% | OK |

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@
## Phase 3: [US1] Scheduled Backups
- [x] T009 [US1] Implement schedule loading and registration logic in `SchedulerService`
- [x] T010 [US1] Update `Environment` settings API to handle `backup_schedule` updates in `backend/src/api/routes/environments.py`
- [x] T011 [P] [US1] Add schedule configuration fields to Environment edit form in `frontend/src/components/EnvSelector.svelte` (or appropriate component)
- [ ] T011 [P] [US1] Add schedule configuration fields to Environment edit form in `frontend/src/components/EnvSelector.svelte` (or appropriate component)
- [x] T012 [US1] Implement validation for Cron expressions in backend and frontend
## Phase 4: [US2] Unified Task Management UI
@@ -34,8 +34,8 @@
- [x] T021 [US4] Integrate log viewer into TaskList or as a separate modal/page
## Final Phase: Polish & Cross-cutting concerns
- [x] T022 Implement task cleanup/retention policy (e.g., delete tasks older than 30 days)
- [ ] T023 Add real-time updates for task status using WebSockets (optional/refinement)
- [ ] T022 Implement task cleanup/retention policy (e.g., delete tasks older than 30 days)
- [x] T023 Add real-time updates for task status using WebSockets (optional/refinement)
- [x] T024 Ensure consistent error handling and logging across scheduler and task manager
## Dependencies

View File

@@ -0,0 +1,42 @@
# Checklist: Frontend UI & i18n Requirements Quality
**Purpose**: Validate the quality, clarity, and completeness of the requirements for the unified frontend CSS and localization feature.
**Meta**:
- **Feature**: 013-unify-frontend-css
- **Created**: 2026-01-23
- **Focus**: UX Consistency, Component Contracts, i18n Completeness
## Requirement Completeness
- [x] CHK001 Are all necessary UI components (Button, Input, Select, Card, PageHeader) explicitly identified for standardization? [Completeness, Spec §FR-002]
- [x] CHK002 Are the supported languages (RU, EN) and default language (RU) explicitly defined? [Completeness, Spec §FR-008, §FR-009]
- [x] CHK003 Is the persistence mechanism for language selection (LocalStorage) specified? [Completeness, Spec §FR-010]
- [x] CHK004 Are the specific pages (Dashboard, Settings) targeted for migration identified? [Completeness, Spec §FR-005]
## Requirement Clarity
- [x] CHK005 Is the "consistent spacing" requirement quantified with specific values or a system (e.g., Tailwind scale)? [Clarity, Spec §FR-004]
- [x] CHK006 Are the specific visual properties (color, padding, hover state) that must be consistent defined in the design system configuration? [Clarity, Spec §SC-001]
- [x] CHK007 Is the behavior of "legacy components" clearly defined (refactor vs. approximate)? [Clarity, Spec Edge Cases]
## Requirement Consistency
- [x] CHK008 Do the component contracts in `contracts/ui-components.md` align with the visual requirements in the spec? [Consistency]
- [x] CHK009 Is the decision to use Svelte stores for i18n consistent with the requirement for client-side persistence? [Consistency, Research §2]
## Acceptance Criteria Quality
- [x] CHK010 Can the "consistent visual experience" be objectively verified through the defined independent tests? [Measurability, Spec §User Story 1]
- [x] CHK011 Is the "0 instances of arbitrary hardcoded color values" criterion measurable via static analysis or search? [Measurability, Spec §SC-003]
- [x] CHK012 Is the language persistence requirement testable by reloading the page? [Measurability, Spec §SC-006]
## Scenario Coverage
- [x] CHK013 Are requirements defined for the scenario where a translation key is missing? [Coverage, Spec Edge Cases]
- [x] CHK014 Are requirements defined for the initial load state (default language)? [Coverage, Spec §User Story 3]
- [x] CHK015 Are requirements defined for switching languages while on a page with dynamic content? [Coverage, Gap]
## Edge Case Coverage
- [x] CHK016 Is the behavior defined for text that overflows when translated to a longer language (e.g., RU vs EN)? [Edge Case, Gap]
- [x] CHK017 Is the behavior defined if LocalStorage is disabled or inaccessible? [Edge Case, Gap]
- [x] CHK018 Are requirements defined for responsive behavior on mobile devices? [Edge Case, Spec §Edge Cases]
## Non-Functional Requirements
- [x] CHK019 Are there specific performance targets for language switching (e.g., "instant", "no layout shift")? [NFR, Research §Technical Context]
- [x] CHK020 Are accessibility requirements (ARIA labels, keyboard nav) defined for the new components? [NFR, Gap]

View File

@@ -0,0 +1,50 @@
# UI Component Contracts
This document defines the strict API contracts for the standardized UI components.
## Button Component
### Description
A standard interactive button element that triggers an action.
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `'primary' \| 'secondary' \| 'danger' \| 'ghost'` | `'primary'` | Visual style of the button. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size of the button. |
| `isLoading` | `boolean` | `false` | If true, shows a spinner and disables interaction. |
| `disabled` | `boolean` | `false` | If true, prevents interaction. |
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | HTML button type. |
| `onclick` | `(event: MouseEvent) => void` | `undefined` | Click handler. |
| `class` | `string` | `''` | Additional CSS classes (use sparingly). |
### Slots
- `default`: The content of the button (text or icon).
## Input Component
### Description
A standard text input field with support for labels and error messages.
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `label` | `string` | `undefined` | Text label displayed above the input. |
| `value` | `string` | `''` | The current value (bindable). |
| `placeholder` | `string` | `''` | Placeholder text. |
| `error` | `string` | `undefined` | Error message displayed below the input. |
| `disabled` | `boolean` | `false` | If true, prevents interaction. |
| `type` | `'text' \| 'password' \| 'email' \| 'number'` | `'text'` | HTML input type. |
## Select Component
### Description
A standard dropdown selection component.
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `label` | `string` | `undefined` | Text label displayed above the select. |
| `value` | `string \| number` | `''` | The selected value (bindable). |
| `options` | `{ value: string \| number, label: string }[]` | `[]` | List of options to display. |
| `disabled` | `boolean` | `false` | If true, prevents interaction. |

View File

@@ -0,0 +1,59 @@
# Data Model: Frontend State & Components
## 1. i18n State (Client-Side)
The application state for internationalization is transient (in-memory) but initialized from and persisted to `localStorage`.
### Entities
#### `Locale`
- **Type**: String Enum
- **Values**: `"ru"`, `"en"`
- **Default**: `"ru"`
- **Persistence**: `localStorage.getItem("locale")`
#### `TranslationDictionary`
- **Type**: JSON Object
- **Structure**: Nested key-value pairs where values are strings.
- **Example**:
```json
{
"common": {
"save": "Сохранить",
"cancel": "Отмена"
},
"dashboard": {
"title": "Панель управления"
}
}
```
## 2. Component Props (Contracts)
These define the "API" for the standardized UI components.
### `Button`
- **variant**: `"primary" | "secondary" | "danger" | "ghost"` (default: "primary")
- **size**: `"sm" | "md" | "lg"` (default: "md")
- **isLoading**: `boolean` (default: false)
- **disabled**: `boolean` (default: false)
- **type**: `"button" | "submit" | "reset"` (default: "button")
- **onClick**: `() => void` (optional)
### `Input`
- **label**: `string` (optional)
- **value**: `string` (two-way binding)
- **placeholder**: `string` (optional)
- **error**: `string` (optional)
- **disabled**: `boolean` (default: false)
- **type**: `"text" | "password" | "email" | "number"` (default: "text")
### `Card`
- **title**: `string` (optional)
- **padding**: `"none" | "sm" | "md" | "lg"` (default: "md")
### `Select`
- **options**: `Array<{ value: string | number, label: string }>`
- **value**: `string | number` (two-way binding)
- **label**: `string` (optional)
- **disabled**: `boolean` (default: false)

View File

@@ -0,0 +1,70 @@
# Implementation Plan: [FEATURE]
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
## Summary
Unify frontend styling using a centralized Tailwind CSS configuration and a set of standardized Svelte wrapper components. Implement internationalization (i18n) with support for Russian (default) and English, persisting language preference in LocalStorage.
## Technical Context
**Language/Version**: Node.js 18+ (Frontend Build), Svelte 5.x
**Primary Dependencies**: SvelteKit, Tailwind CSS, `date-fns` (existing)
**Storage**: LocalStorage (for language preference)
**Testing**: Manual verification per User Scenarios (Visual Regression)
**Target Platform**: Web Browser (Responsive)
**Project Type**: Web application (Frontend)
**Performance Goals**: Instant language switching, zero layout shift
**Constraints**: Must support existing pages without breaking layout
**Scale/Scope**: ~10-15 core UI components, global CSS update
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- **Semantic Protocol Compliance**: PASS. New Svelte components must follow the Component Header standard.
- **Causal Validity**: PASS. Design System (Tokens) and Component Contracts will be defined before implementation.
- **Immutability of Architecture**: PASS. No changes to backend architecture; strictly frontend presentation layer.
- **Design by Contract**: PASS. Components will define strict props/interfaces.
- **Everything is a Plugin**: N/A. UI components are not backend plugins, but will be modular.
## Project Structure
### Documentation (this feature)
```text
specs/[###-feature]/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
```
### Source Code (repository root)
```text
frontend/
├── src/
│ ├── lib/
│ │ ├── i18n/ # [NEW] Translation dictionaries and stores
│ │ └── ui/ # [NEW] Standardized Svelte components
│ ├── components/ # [EXISTING] To be refactored to use lib/ui
│ ├── routes/ # [EXISTING] Pages
│ └── app.css # [EXISTING] Global styles (Tailwind directives)
```
**Structure Decision**: Adopting a standard SvelteKit structure where reusable UI components and logic (i18n) reside in `src/lib`, accessible via `$lib` alias. Existing `components` directory will be gradually refactored or moved to `lib/ui` if generic enough.
## Complexity Tracking
> **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |

View File

@@ -0,0 +1,63 @@
# Quickstart: Frontend Development
## Prerequisites
- Node.js 18+
- npm
## Setup
1. Navigate to the frontend directory:
```bash
cd frontend
```
2. Install dependencies:
```bash
npm install
```
## Running the Development Server
```bash
npm run dev
```
The application will be available at `http://localhost:5173`.
## Using Standard UI Components
Import components from `$lib/ui`:
```svelte
<script>
import { Button, Input } from '$lib/ui';
</script>
<Input label="Username" bind:value={username} />
<Button variant="primary" onclick={submit}>Submit</Button>
```
## Using Internationalization
Import the `t` store from `$lib/i18n`:
```svelte
<script>
import { t } from '$lib/i18n';
</script>
<h1>{$t.dashboard.title}</h1>
<button>{$t.common.save}</button>
```
## Adding New Translations
1. Open `frontend/src/lib/i18n/locales/ru.json` and add the new key-value pair.
2. Open `frontend/src/lib/i18n/locales/en.json` and add the corresponding English translation.
## Adding New Components
1. Create a new `.svelte` file in `frontend/src/lib/ui/`.
2. Define props and styles using Tailwind CSS.
3. Export the component in `frontend/src/lib/ui/index.ts`.

Some files were not shown because too many files have changed in this diff Show More