Выполнено, передано на тестирование

This commit is contained in:
2026-01-26 21:17:05 +03:00
parent da34deac02
commit 16ffeb1ed6
12 changed files with 156 additions and 368 deletions

View File

@@ -1,60 +0,0 @@
<!-- [DEF:EnvSelector:Component] -->
<!--
@SEMANTICS: environment, selector, dropdown, migration
@PURPOSE: Provides a UI component for selecting source and target environments.
@LAYER: Feature
@RELATION: BINDS_TO -> environments store
@INVARIANT: Source and target environments must be selectable from the list of configured environments.
-->
<script lang="ts">
// [SECTION: IMPORTS]
import { onMount, createEventDispatcher } from 'svelte';
// [/SECTION]
// [SECTION: PROPS]
export let label: string = "Select Environment";
export let selectedId: string = "";
export let environments: Array<{id: string, name: string, url: string}> = [];
// [/SECTION]
const dispatch = createEventDispatcher();
// [DEF:handleSelect:Function]
/**
* @purpose Dispatches the selection change event.
* @pre event.target must be an HTMLSelectElement.
* @post selectedId is updated and 'change' event is dispatched.
* @param {Event} event - The change event from the select element.
*/
function handleSelect(event: Event) {
const target = event.target as HTMLSelectElement;
selectedId = target.value;
dispatch('change', { id: selectedId });
}
// [/DEF:handleSelect:Function]
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="flex flex-col space-y-1">
<label for="env-select" class="text-sm font-medium text-gray-700">{label}</label>
<select
id="env-select"
class="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"
value={selectedId}
on:change={handleSelect}
>
<option value="" disabled>-- Choose an environment --</option>
{#each environments as env}
<option value={env.id}>{env.name} ({env.url})</option>
{/each}
</select>
</div>
<!-- [/SECTION] -->
<style>
/* Component specific styles */
</style>
<!-- [/DEF:EnvSelector:Component] -->

View File

@@ -25,35 +25,12 @@
> >
{$t.nav.dashboard} {$t.nav.dashboard}
</a> </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' : ''}"
>
{$t.nav.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' : ''}"
>
{$t.nav.git}
</a>
<a <a
href="/tasks" 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' : ''}" class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname.startsWith('/tasks') ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
> >
{$t.nav.tasks} {$t.nav.tasks}
</a> </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' : ''}">
{$t.nav.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">{$t.nav.tools_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">{$t.nav.tools_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">{$t.nav.tools_debug}</a>
<a href="/tools/storage" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.tools_storage}</a>
</div>
</div>
<div class="relative inline-block group"> <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' : ''}"> <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' : ''}">
{$t.nav.settings} {$t.nav.settings}
@@ -62,7 +39,6 @@
<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" 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/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> <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>
<a href="/settings/environments" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.settings_environments}</a>
</div> </div>
</div> </div>
<LanguageSwitcher /> <LanguageSwitcher />

View File

@@ -0,0 +1,74 @@
<!-- [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';
// [/SECTION]
// [SECTION: PROPS]
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">
{$t.storage.table.download}
</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

@@ -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

@@ -27,6 +27,12 @@
goto('/migration'); goto('/migration');
} else if (plugin.id === 'git-integration') { } else if (plugin.id === 'git-integration') {
goto('/git'); goto('/git');
} else if (plugin.id === 'superset-backup') {
goto('/tools/backups');
} else if (plugin.id === 'superset-storage') {
goto('/tools/storage');
} else if (plugin.id === 'superset-mapper') {
goto('/tools/mapper');
} else { } else {
selectedPlugin.set(plugin); selectedPlugin.set(plugin);
} }
@@ -82,7 +88,7 @@
{/if} {/if}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each data.plugins as plugin} {#each data.plugins.filter(p => p.id !== 'superset-search') as plugin}
<div <div
on:click={() => selectPlugin(plugin)} on:click={() => selectPlugin(plugin)}
role="button" role="button"

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

@@ -116,13 +116,7 @@
</script> </script>
<div class="container mx-auto p-4 max-w-6xl"> <div class="container mx-auto p-4 max-w-6xl">
<PageHeader title={$t.tasks.management}> <PageHeader title={$t.tasks.management} />
<div slot="actions">
<Button on:click={() => showBackupModal = true}>
{$t.tasks.run_backup}
</Button>
</div>
</PageHeader>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-1"> <div class="lg:col-span-1">

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

@@ -1,25 +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';
import { PageHeader } from '$lib/ui';
</script>
<div class="max-w-7xl mx-auto p-6">
<PageHeader title="Dataset Search" />
<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>
<!-- [/DEF:SearchPage:Component] -->

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

@@ -8,18 +8,18 @@
*Goal: Initialize project structure for the new feature.* *Goal: Initialize project structure for the new feature.*
- [ ] T001 Create backup component directory structure - [x] T001 Create backup component directory structure
- Path: `frontend/src/components/backups/` - Path: `frontend/src/components/backups/`
- [ ] T002 Create backup page route directory - [x] T002 Create backup page route directory
- Path: `frontend/src/routes/tools/backups/` - Path: `frontend/src/routes/tools/backups/`
## Phase 2: Foundational ## Phase 2: Foundational
*Goal: Prepare core components for integration and verify backend connectivity.* *Goal: Prepare core components for integration and verify backend connectivity.*
- [ ] T003 Verify backend API endpoints for backups (via `009-backup-scheduler`) - [x] T003 Verify backend API endpoints for backups (via `009-backup-scheduler`)
- Path: `backend/src/api/routes/tasks.py` (or relevant backup route) - Path: `backend/src/api/routes/tasks.py` (or relevant backup route)
- [ ] T004 Define Backup types and interfaces in frontend - [x] T004 Define Backup types and interfaces in frontend
- Path: `frontend/src/types/backup.ts` - Path: `frontend/src/types/backup.ts`
## Phase 3: User Story 1 - Centralized Tool Access via Dashboard ## Phase 3: User Story 1 - Centralized Tool Access via Dashboard
@@ -28,11 +28,11 @@
**User Story**: As a user, I want to access all main tools (Backups, Mapper, Storage) from the main Dashboard so that I have a central hub for operations. (P1) **User Story**: As a user, I want to access all main tools (Backups, Mapper, Storage) from the main Dashboard so that I have a central hub for operations. (P1)
- [ ] T005 [US1] Update DashboardGrid to include "Backup Manager" card - [x] T005 [US1] Update DashboardGrid to include "Backup Manager" card
- Path: `frontend/src/components/DashboardGrid.svelte` - Path: `frontend/src/components/DashboardGrid.svelte`
- [ ] T006 [US1] Update DashboardGrid to ensure "Dataset Mapper" and "Storage Manager" cards are present - [x] T006 [US1] Update DashboardGrid to ensure "Dataset Mapper" and "Storage Manager" cards are present
- Path: `frontend/src/components/DashboardGrid.svelte` - Path: `frontend/src/components/DashboardGrid.svelte`
- [ ] T007 [US1] Remove "Dataset Search" card from DashboardGrid - [x] T007 [US1] Remove "Dataset Search" card from DashboardGrid
- Path: `frontend/src/components/DashboardGrid.svelte` - Path: `frontend/src/components/DashboardGrid.svelte`
## Phase 4: User Story 2 - Simplified Navigation Bar ## Phase 4: User Story 2 - Simplified Navigation Bar
@@ -41,15 +41,15 @@
**User Story**: As a user, I want a clean Navbar containing only global context items (Tasks, Settings) so that the interface is less cluttered and navigation is distinct from tool usage. (P1) **User Story**: As a user, I want a clean Navbar containing only global context items (Tasks, Settings) so that the interface is less cluttered and navigation is distinct from tool usage. (P1)
- [ ] T008 [US2] Remove "Dataset Search" link from Navbar - [x] T008 [US2] Remove "Dataset Search" link from Navbar
- Path: `frontend/src/components/Navbar.svelte` - Path: `frontend/src/components/Navbar.svelte`
- [ ] T009 [US2] Remove "Dataset Mapper" link from Navbar - [x] T009 [US2] Remove "Dataset Mapper" link from Navbar
- Path: `frontend/src/components/Navbar.svelte` - Path: `frontend/src/components/Navbar.svelte`
- [ ] T010 [US2] Remove "Deployment Environments" link from Navbar - [x] T010 [US2] Remove "Deployment Environments" link from Navbar
- Path: `frontend/src/components/Navbar.svelte` - Path: `frontend/src/components/Navbar.svelte`
- [ ] T011 [US2] Verify "Tasks" and "Settings" links remain in Navbar - [x] T011 [US2] Verify "Tasks" and "Settings" links remain in Navbar
- Path: `frontend/src/components/Navbar.svelte` - Path: `frontend/src/components/Navbar.svelte`
- [ ] T012 [US2] Remove "Run backup" button from Tasks page - [x] T012 [US2] Remove "Run backup" button from Tasks page
- Path: `frontend/src/routes/tasks/+page.svelte` (or relevant component) - Path: `frontend/src/routes/tasks/+page.svelte` (or relevant component)
## Phase 5: Backup Management UI ## Phase 5: Backup Management UI
@@ -58,15 +58,15 @@
**User Story**: (Implicit P1 from FR-004) System MUST provide access to the full Backup Management component via the Dashboard link. **User Story**: (Implicit P1 from FR-004) System MUST provide access to the full Backup Management component via the Dashboard link.
- [ ] T013 [US1] Create BackupList component to display existing backups (Must use `src/lib/ui` components and `src/lib/i18n`) - [x] T013 [US1] Create BackupList component to display existing backups (Must use `src/lib/ui` components and `src/lib/i18n`)
- Path: `frontend/src/components/backups/BackupList.svelte` - Path: `frontend/src/components/backups/BackupList.svelte`
- [ ] T014 [US1] Create BackupManager main component (container) (Must use `src/lib/ui` components and `src/lib/i18n`) - [x] T014 [US1] Create BackupManager main component (container) (Must use `src/lib/ui` components and `src/lib/i18n`)
- Path: `frontend/src/components/backups/BackupManager.svelte` - Path: `frontend/src/components/backups/BackupManager.svelte`
- [ ] T015 [US1] Implement "Create Backup" functionality in BackupManager (Must use `src/lib/ui` components and `src/lib/i18n`) - [x] T015 [US1] Implement "Create Backup" functionality in BackupManager (Must use `src/lib/ui` components and `src/lib/i18n`)
- Path: `frontend/src/components/backups/BackupManager.svelte` - Path: `frontend/src/components/backups/BackupManager.svelte`
- [ ] T016 [US1] Implement "Restore Backup" functionality (if supported by backend) (Must use `src/lib/ui` components and `src/lib/i18n`) - [x] T016 [US1] Implement "Restore Backup" functionality (if supported by backend) (Must use `src/lib/ui` components and `src/lib/i18n`)
- Path: `frontend/src/components/backups/BackupManager.svelte` - Path: `frontend/src/components/backups/BackupManager.svelte`
- [ ] T017 [US1] Create Backup page to host the manager (Must use `src/lib/ui` components and `src/lib/i18n`) - [x] T017 [US1] Create Backup page to host the manager (Must use `src/lib/ui` components and `src/lib/i18n`)
- Path: `frontend/src/routes/tools/backups/+page.svelte` - Path: `frontend/src/routes/tools/backups/+page.svelte`
## Phase 6: User Story 3 - Deprecation ## Phase 6: User Story 3 - Deprecation
@@ -75,26 +75,26 @@
**User Story**: As a user, I want removed features (Dataset Search, Deployment Environments) to be inaccessible so that I don't use deprecated workflows. (P2) **User Story**: As a user, I want removed features (Dataset Search, Deployment Environments) to be inaccessible so that I don't use deprecated workflows. (P2)
- [ ] T018 [US3] Delete Dataset Search route - [x] T018 [US3] Delete Dataset Search route
- Path: `frontend/src/routes/tools/search/` (delete directory) - Path: `frontend/src/routes/tools/search/` (delete directory)
- [ ] T019 [US3] Delete Deployment Environments route - [x] T019 [US3] Delete Deployment Environments route
- Path: `frontend/src/routes/settings/environments/` (delete directory) - Path: `frontend/src/routes/settings/environments/` (delete directory)
- [ ] T020 [US3] Delete Dataset Search component (if not used elsewhere) - [x] T020 [US3] Delete Dataset Search component (if not used elsewhere)
- Path: `frontend/src/components/tools/SearchTool.svelte` - Path: `frontend/src/components/tools/SearchTool.svelte`
- [ ] T021 [US3] Delete EnvSelector component (if not used elsewhere) - [x] T021 [US3] Delete EnvSelector component (if not used elsewhere)
- Path: `frontend/src/components/EnvSelector.svelte` - Path: `frontend/src/components/EnvSelector.svelte`
## Phase 7: Polish & Cross-Cutting ## Phase 7: Polish & Cross-Cutting
*Goal: Final verification and cleanup.* *Goal: Final verification and cleanup.*
- [ ] T022 Verify all navigation links work correctly - [x] T022 Verify all navigation links work correctly
- Path: `frontend/src/components/Navbar.svelte` - Path: `frontend/src/components/Navbar.svelte`
- [ ] T023 Verify responsive layout of new Dashboard grid - [x] T023 Verify responsive layout of new Dashboard grid
- Path: `frontend/src/components/DashboardGrid.svelte` - Path: `frontend/src/components/DashboardGrid.svelte`
- [ ] T024 Ensure i18n strings are extracted for new Backup UI - [x] T024 Ensure i18n strings are extracted for new Backup UI
- Path: `frontend/src/lib/i18n/` (or relevant locale files) - Path: `frontend/src/lib/i18n/` (or relevant locale files)
- [ ] T025 Verify "Run backup" action successfully triggers backup job (Manual/E2E check) - [x] T025 Verify "Run backup" action successfully triggers backup job (Manual/E2E check)
- Path: `frontend/src/components/backups/BackupManager.svelte` - Path: `frontend/src/components/backups/BackupManager.svelte`
## Dependencies ## Dependencies