Compare commits

..

5 Commits

49 changed files with 1849 additions and 375 deletions

5
.gitignore vendored
View File

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

View File

@@ -29,6 +29,9 @@ Auto-generated from all feature plans. Last updated: 2025-12-19
- LocalStorage (for language preference) (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) - 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) - 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) - Python 3.9+ (Backend), Node.js 18+ (Frontend Build) (001-plugin-arch-svelte-ui)
@@ -49,9 +52,9 @@ cd src; pytest; ruff check .
Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions
## Recent Changes ## Recent Changes
- 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) - 014-file-storage-ui: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI (Backend), SvelteKit (Frontend)
- 013-unify-frontend-css: Added Node.js 18+ (Frontend Build), Svelte 5.x + SvelteKit, Tailwind CSS, `date-fns` (existing)
- 011-git-integration-dashboard: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI, SvelteKit, GitPython (or CLI git), Pydantic, SQLAlchemy, Superset API
<!-- MANUAL ADDITIONS START --> <!-- MANUAL ADDITIONS START -->

View File

@@ -1,11 +1,10 @@
<!-- <!--
SYNC IMPACT REPORT SYNC IMPACT REPORT
Version: 1.7.1 (Simplified Workflow) Version: 1.8.0 (Frontend Unification)
Changes: Changes:
- Simplified Generation Workflow to a single phase: Code Generation from `tasks.md`. - Added Principle VIII: Unified Frontend Experience (Mandating Design System & i18n).
- Removed multi-phase Architecture/Implementation split to streamline development.
Templates Status: Templates Status:
- .specify/templates/plan-template.md: ✅ Aligned (Dynamic check). - .specify/templates/plan-template.md: ✅ Aligned.
- .specify/templates/spec-template.md: ✅ Aligned. - .specify/templates/spec-template.md: ✅ Aligned.
- .specify/templates/tasks-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 ### 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. 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 ## File Structure Standards
Refer to **Section III (File Structure Standard)** in `semantic_protocol.md` for the authoritative definitions of: Refer to **Section III (File Structure Standard)** in `semantic_protocol.md` for the authoritative definitions of:
- Python Module Headers (`.py`) - 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. - **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. - **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

View File

@@ -23,7 +23,7 @@ router = APIRouter()
# [DEF:ScheduleSchema:DataClass] # [DEF:ScheduleSchema:DataClass]
class ScheduleSchema(BaseModel): class ScheduleSchema(BaseModel):
enabled: bool = False 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:ScheduleSchema:DataClass]
# [DEF:EnvironmentResponse:DataClass] # [DEF:EnvironmentResponse:DataClass]

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Any from typing import Dict, Any, Optional
from .logger import belief_scope from .logger import belief_scope
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -68,6 +68,21 @@ class PluginBase(ABC):
pass pass
# [/DEF:version:Function] # [/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 @abstractmethod
# [DEF:get_schema:Function] # [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for the plugin's input parameters. # @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") name: str = Field(..., description="Human-readable name for the plugin")
description: str = Field(..., description="Brief description of what the plugin does") description: str = Field(..., description="Brief description of what the plugin does")
version: str = Field(..., description="Version of the plugin") 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") input_schema: Dict[str, Any] = Field(..., description="JSON schema for input parameters", alias="schema")
# [/DEF:PluginConfig:Class] # [/DEF:PluginConfig:Class]

View File

@@ -141,6 +141,7 @@ class PluginLoader:
name=plugin_instance.name, name=plugin_instance.name,
description=plugin_instance.description, description=plugin_instance.description,
version=plugin_instance.version, version=plugin_instance.version,
ui_route=plugin_instance.ui_route,
schema=schema, schema=schema,
) )
# The following line is commented out because it requires a schema to be passed to validate against. # The following line is commented out because it requires a schema to be passed to validate against.

View File

@@ -75,6 +75,15 @@ class BackupPlugin(PluginBase):
return "1.0.0" return "1.0.0"
# [/DEF:version:Function] # [/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] # [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for backup plugin parameters. # @PURPOSE: Returns the JSON schema for backup plugin parameters.
# @PRE: Plugin instance exists. # @PRE: Plugin instance exists.

View File

@@ -63,6 +63,15 @@ class DebugPlugin(PluginBase):
return "1.0.0" return "1.0.0"
# [/DEF:version:Function] # [/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] # [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for the debug plugin parameters. # @PURPOSE: Returns the JSON schema for the debug plugin parameters.
# @PRE: Plugin instance exists. # @PRE: Plugin instance exists.

View File

@@ -99,6 +99,15 @@ class GitPlugin(PluginBase):
return "0.1.0" return "0.1.0"
# [/DEF:version:Function] # [/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] # [DEF:get_schema:Function]
# @PURPOSE: Возвращает JSON-схему параметров для выполнения задач плагина. # @PURPOSE: Возвращает JSON-схему параметров для выполнения задач плагина.
# @PRE: GitPlugin is initialized. # @PRE: GitPlugin is initialized.

View File

@@ -66,6 +66,15 @@ class MapperPlugin(PluginBase):
return "1.0.0" return "1.0.0"
# [/DEF:version:Function] # [/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] # [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for the mapper plugin parameters. # @PURPOSE: Returns the JSON schema for the mapper plugin parameters.
# @PRE: Plugin instance exists. # @PRE: Plugin instance exists.

View File

@@ -71,6 +71,15 @@ class MigrationPlugin(PluginBase):
return "1.0.0" return "1.0.0"
# [/DEF:version:Function] # [/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] # [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for migration plugin parameters. # @PURPOSE: Returns the JSON schema for migration plugin parameters.
# @PRE: Config manager is available. # @PRE: Config manager is available.

View File

@@ -64,6 +64,15 @@ class SearchPlugin(PluginBase):
return "1.0.0" return "1.0.0"
# [/DEF:version:Function] # [/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] # [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for the search plugin parameters. # @PURPOSE: Returns the JSON schema for the search plugin parameters.
# @PRE: Plugin instance exists. # @PRE: Plugin instance exists.

View File

@@ -82,6 +82,15 @@ class StoragePlugin(PluginBase):
return "1.0.0" return "1.0.0"
# [/DEF:version:Function] # [/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] # [DEF:get_schema:Function]
# @PURPOSE: Returns the JSON schema for storage plugin parameters. # @PURPOSE: Returns the JSON schema for storage plugin parameters.
# @PRE: None. # @PRE: None.

Binary file not shown.

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

@@ -13,6 +13,8 @@
import { getConnections } from '../../services/connectionService.js'; import { getConnections } from '../../services/connectionService.js';
import { selectedTask } from '../../lib/stores.js'; import { selectedTask } from '../../lib/stores.js';
import { addToast } from '../../lib/toasts.js'; import { addToast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Card, Select, Input } from '../../lib/ui';
// [/SECTION] // [/SECTION]
let envs = []; let envs = [];
@@ -36,7 +38,7 @@
envs = await envsRes.json(); envs = await envsRes.json();
connections = await getConnections(); connections = await getConnections();
} catch (e) { } catch (e) {
addToast('Failed to fetch data', 'error'); addToast($t.mapper.errors.fetch_failed, 'error');
} }
} }
// [/DEF:fetchData:Function] // [/DEF:fetchData:Function]
@@ -47,17 +49,17 @@
// @POST: Mapper task is started and selectedTask is updated. // @POST: Mapper task is started and selectedTask is updated.
async function handleRunMapper() { async function handleRunMapper() {
if (!selectedEnv || !datasetId) { if (!selectedEnv || !datasetId) {
addToast('Please fill in required fields', 'warning'); addToast($t.mapper.errors.required_fields, 'warning');
return; return;
} }
if (source === 'postgres' && (!selectedConnection || !tableName)) { if (source === 'postgres' && (!selectedConnection || !tableName)) {
addToast('Connection and Table Name are required for postgres source', 'warning'); addToast($t.mapper.errors.postgres_required, 'warning');
return; return;
} }
if (source === 'excel' && !excelPath) { if (source === 'excel' && !excelPath) {
addToast('Excel path is required for excel source', 'warning'); addToast($t.mapper.errors.excel_required, 'warning');
return; return;
} }
@@ -75,7 +77,7 @@
}); });
selectedTask.set(task); selectedTask.set(task);
addToast('Mapper task started', 'success'); addToast($t.mapper.success.started, 'success');
} catch (e) { } catch (e) {
addToast(e.message, 'error'); addToast(e.message, 'error');
} finally { } finally {
@@ -88,78 +90,94 @@
</script> </script>
<!-- [SECTION: TEMPLATE] --> <!-- [SECTION: TEMPLATE] -->
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div class="space-y-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Dataset Column Mapper</h3> <Card title={$t.mapper.title}>
<div class="space-y-4"> <div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-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> <div>
<label for="mapper-conn" class="block text-sm font-medium text-gray-700">Saved Connection</label> <Select
<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"> label={$t.mapper.environment}
<option value="" disabled>-- Select Connection --</option> bind:value={selectedEnv}
{#each connections as conn} options={[
<option value={conn.id}>{conn.name}</option> { value: '', label: $t.mapper.select_env },
{/each} ...envs.map(e => ({ value: e.id, label: e.name }))
</select> ]}
/>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div>
<div> <Input
<label for="mapper-table" class="block text-sm font-medium text-gray-700">Table Name</label> label={$t.mapper.dataset_id}
<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" /> type="number"
</div> bind:value={datasetId}
<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> </div>
</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"> <div>
<button <label class="block text-sm font-medium text-gray-700 mb-2">{$t.mapper.source}</label>
on:click={handleRunMapper} <div class="flex space-x-4">
disabled={isRunning} <label class="inline-flex items-center">
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" <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>
{isRunning ? 'Starting...' : 'Run Mapper'} </label>
</button> <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>
</div> </Card>
</div> </div>
<!-- [/SECTION] --> <!-- [/SECTION] -->
<!-- [/DEF:MapperTool:Component] --> <!-- [/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); const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); 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(); return await response.json();
} catch (error) { } catch (error) {
@@ -132,6 +135,7 @@ export const api = {
// [/DEF:api_module:Module] // [/DEF:api_module:Module]
// Export individual functions for easier use in components // Export individual functions for easier use in components
export { requestApi };
export const getPlugins = api.getPlugins; export const getPlugins = api.getPlugins;
export const getTasks = api.getTasks; export const getTasks = api.getTasks;
export const getTask = api.getTask; export const getTask = api.getTask;

View File

@@ -23,10 +23,8 @@
*/ */
function selectPlugin(plugin) { function selectPlugin(plugin) {
console.log(`[Dashboard][Action] Selecting plugin: ${plugin.id}`); console.log(`[Dashboard][Action] Selecting plugin: ${plugin.id}`);
if (plugin.id === 'superset-migration') { if (plugin.ui_route) {
goto('/migration'); goto(plugin.ui_route);
} else if (plugin.id === 'git-integration') {
goto('/git');
} else { } else {
selectedPlugin.set(plugin); selectedPlugin.set(plugin);
} }
@@ -82,7 +80,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

@@ -13,6 +13,7 @@
<script lang="ts"> <script lang="ts">
// [SECTION: IMPORTS] // [SECTION: IMPORTS]
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores';
import { listFiles, deleteFile } from '../../../services/storageService'; import { listFiles, deleteFile } from '../../../services/storageService';
import { addToast } from '../../../lib/toasts'; import { addToast } from '../../../lib/toasts';
import { t } from '../../../lib/i18n'; import { t } from '../../../lib/i18n';
@@ -90,6 +91,8 @@
// [DEF:navigateUp:Function] // [DEF:navigateUp:Function]
/** /**
* @purpose Navigates one level up in the directory structure. * @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() { function navigateUp() {
if (!currentPath || currentPath === activeTab) return; if (!currentPath || currentPath === activeTab) return;
@@ -100,7 +103,18 @@
} }
// [/DEF:navigateUp:Function] // [/DEF:navigateUp:Function]
onMount(loadFiles); onMount(() => {
const pathParam = $page.url.searchParams.get('path');
if (pathParam) {
currentPath = pathParam;
if (pathParam.startsWith('repositorys')) {
activeTab = 'repositorys';
} else {
activeTab = 'backups';
}
}
loadFiles();
});
$: if (activeTab) { $: if (activeTab) {
// Reset path when switching tabs // Reset path when switching tabs

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

@@ -35,7 +35,7 @@
## Final Phase: Polish & Cross-cutting concerns ## Final Phase: Polish & Cross-cutting concerns
- [ ] T022 Implement task cleanup/retention policy (e.g., delete tasks older than 30 days) - [ ] 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) - [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 - [x] T024 Ensure consistent error handling and logging across scheduler and task manager
## Dependencies ## Dependencies

View File

@@ -0,0 +1,34 @@
# Specification Quality Checklist: Frontend Navigation Redesign
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-01-26
**Feature**: [specs/015-frontend-nav-redesign/spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`

View File

@@ -0,0 +1,18 @@
# Backup Contracts
## Component: BackupManager
### Props
None (Top-level page component)
### Events
- `on:backup-create`: Triggered when user requests a new backup.
- `on:backup-restore`: Triggered when user requests a restore.
### Data Dependencies
- `GET /api/environments`: Fetch list of available environments.
- `GET /api/storage/files?category=backups`: Fetch list of backup files.
- `POST /api/tasks`: Create new backup task.
- Body: `{ plugin_id: 'superset-backup', params: { environment_id: string } }`
- `PUT /api/environments/{id}/schedule`: Update backup schedule.
- Body: `{ enabled: boolean, cron_expression: string }`

View File

@@ -0,0 +1,47 @@
# Data Model: Frontend Navigation Redesign
## Plugin Configuration
The `PluginConfig` model is extended to support backend-driven navigation.
```python
class PluginConfig(BaseModel):
"""Pydantic model for plugin configuration."""
id: str = Field(..., description="Unique identifier for the plugin")
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")
```
### ui_route
- **Type**: `Optional[str]`
- **Description**: Specifies the client-side route (URL path) where the plugin's custom UI is hosted.
- **Behavior**:
- If `None` (default): The dashboard will open the plugin using the generic `DynamicForm` modal.
- If set (e.g., `"/tools/mapper"`): The dashboard will navigate (`goto`) to this route when the plugin card is clicked.
## Backup Management (New)
### Backup Types
```typescript
// frontend/src/types/backup.ts
export interface BackupFile {
name: string; // e.g., "prod-dashboard-export-2024.zip"
path: string; // Relative path in storage
size: number; // Bytes
created_at: string; // ISO Date
category: 'backups'; // Fixed category
mime_type?: string;
}
export interface BackupState {
isLoading: boolean;
files: BackupFile[];
error: string | null;
selectedBackup: BackupFile | null;
}

View File

@@ -0,0 +1,88 @@
# Implementation Plan: Frontend Navigation Redesign
**Branch**: `015-frontend-nav-redesign` | **Date**: 2026-01-26 | **Spec**: [specs/015-frontend-nav-redesign/spec.md](../spec.md)
**Input**: Feature specification from `specs/015-frontend-nav-redesign/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
## Summary
This feature redesigns the frontend navigation to shift from a Navbar-heavy approach to a Dashboard-centric model. Key changes include moving tool access (Mapper, Storage, Backups) to the Dashboard, simplifying the Navbar to global contexts (Tasks, Settings), removing deprecated features (Dataset Search, Environments), and implementing a dedicated Backup Management UI based on backend capabilities from feature 009.
Additionally, the navigation architecture is refactored to be backend-driven. Plugins now expose a `ui_route` property, allowing the frontend to dynamically determine the correct navigation path without hardcoded mapping.
## Technical Context
**Language/Version**: Python 3.9+ (Backend), Node.js 18+ (Frontend)
**Primary Dependencies**: FastAPI (Backend), SvelteKit + Tailwind CSS (Frontend)
**Storage**: N/A (UI reorganization and API integration)
**Testing**: Playwright (E2E - if available), Vitest (Unit)
**Target Platform**: Web Browser
**Project Type**: Web Application (Frontend + Backend)
**Performance Goals**: Instant navigation (<100ms), fast dashboard load
**Constraints**: Must maintain responsive design; Backup UI must interface with existing backend endpoints
**Scale/Scope**: ~5-10 file modifications, 1 new major component (BackupManager)
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- [x] **Semantic Protocol Compliance**: All new components will follow Svelte component header standards.
- [x] **Causal Validity**: Contracts (props/events) will be defined before implementation.
- [x] **Immutability of Architecture**: No core architectural changes; only UI reorganization.
- [x] **Design by Contract**: New Backup component will define clear interface contracts.
- [x] **Everything is a Plugin**: N/A (Frontend changes primarily, backend remains plugin-based).
- [x] **Unified Frontend Experience**: All new UI components will use standardized components and internationalization (i18n).
## Project Structure
### Documentation (this feature)
```text
specs/015-frontend-nav-redesign/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output
└── tasks.md # Phase 2 output
```
### Source Code (repository root)
```text
backend/
├── src/
│ ├── core/
│ │ ├── plugin_base.py # (Modify: Add ui_route property)
│ │ └── plugin_loader.py # (Modify: Populate ui_route in PluginConfig)
│ ├── plugins/ # (Modify: Implement ui_route in all plugins)
│ └── api/routes/ # (Verify backup routes exist)
frontend/
├── src/
│ ├── components/
│ │ ├── Navbar.svelte # (Modify: Simplify items)
│ │ ├── DashboardGrid.svelte # (Modify: Add tool links)
│ │ └── backups/ # (New: Backup UI)
│ │ ├── BackupManager.svelte
│ │ └── BackupList.svelte
│ ├── pages/
│ │ └── Dashboard.svelte # (Modify: Layout updates)
│ └── routes/
│ ├── +layout.svelte # (Check global nav injection)
│ ├── +page.svelte # (Modify: Use plugin.ui_route for navigation)
│ └── tools/
│ └── backups/ # (New Route)
│ └── +page.svelte
```
**Structure Decision**: Standard SvelteKit structure. New `backups` component directory for the complex backup UI. Route added under `tools/` to match existing pattern (mapper, storage).
## Complexity Tracking
> **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| N/A | | |

View File

@@ -0,0 +1,29 @@
# Quickstart: Frontend Navigation Redesign
## Overview
This feature reorganizes the application navigation to be dashboard-centric and introduces a dedicated UI for Backup Management.
## New Routes
- `/tools/backups`: The new Backup Management interface.
- `/`: Dashboard (updated with new tool links).
## Removed Routes
- `/tools/search`: Deprecated and removed.
- `/settings/environments`: Deprecated and removed.
## Development
### Running the Backup UI
1. Ensure the backend is running: `cd backend && uvicorn src.app:app --reload`
2. Start the frontend: `cd frontend && npm run dev`
3. Navigate to `http://localhost:5173/tools/backups`
### Key Components
- `frontend/src/components/backups/BackupManager.svelte`: Main container for backup operations.
- `frontend/src/components/DashboardGrid.svelte`: Updated grid with new tool cards.
- `frontend/src/components/Navbar.svelte`: Simplified navigation bar.
## Verification
1. Check Dashboard: Should see cards for Mapper, Storage, and Backups.
2. Check Navbar: Should ONLY show Tasks and Settings.
3. Check Backup Tool: Should load and display backup status/controls.

View File

@@ -0,0 +1,23 @@
# Research: Frontend Navigation Redesign
## Decisions
### 1. Backup Management UI Strategy
**Decision**: Create a dedicated `BackupManager` component in `frontend/src/components/backups/`.
**Rationale**: The requirement is to have a "full component" accessible from the dashboard. Separating it into its own directory ensures modularity and keeps the Dashboard component clean. It will consume the existing backup APIs (likely `/api/tasks` with specific backup types or a dedicated backup endpoint if one exists - *to be verified in Phase 1*).
**Alternatives considered**: Embedding backup controls directly in the Dashboard (rejected: clutters the main view), reusing the TaskRunner component (rejected: need specific backup context/history view).
### 2. Navigation State Management
**Decision**: Use SvelteKit's layout system (`+layout.svelte`) and simple component props for Navbar state.
**Rationale**: The Navbar changes are global. Removing items is a static change to the `Navbar.svelte` component. No complex state management (stores) is needed for this structural change.
**Alternatives considered**: Dynamic config-based menu (rejected: overkill for this specific redesign).
### 3. Deprecation Strategy
**Decision**: Hard removal of "Dataset Search" and "Deployment Environments" components and routes.
**Rationale**: The spec explicitly calls for removal. Keeping dead code increases maintenance burden.
**Alternatives considered**: Hiding behind a feature flag (rejected: requirement is explicit removal).
### 4. Dashboard Grid Layout
**Decision**: Update `DashboardGrid.svelte` to include new cards for Mapper, Storage, and Backups.
**Rationale**: Reusing the existing grid component maintains consistency.
**Alternatives considered**: Creating a separate "Tools" page (rejected: spec requires access from main Dashboard).

View File

@@ -0,0 +1,97 @@
# Feature Specification: Frontend Navigation Redesign
**Feature Branch**: `015-frontend-nav-redesign`
**Created**: 2026-01-26
**Status**: Draft
**Input**: User description: "Я хочу провести редизайн фронта в части навигации. 1. Удалить Dataset Search (из Navbar и дашборда), Deployment Environments 2. Вкладку Tasks оставить для просмотра всех задач - убрать оттуда кнопку Run backup 3. Должен быть полноценный компонент бэкапов, как мы разрабатывали в 009-backup-scheduler. Доступ - из дашборда главного 4. Перенести ссылку на Dataset mapper из Navbar на дашборд 5. Перенести ссылку на Storage manager на дашборд Общая логика - на дашборде должны быть ссылки на полноценные инструменты, навбар - для настроек и общей Tasks"
## Clarifications
### Session 2026-01-26
- Q: Do I need to build the Backup Management UI from scratch? → A: Yes, create the UI for backup using data from task 009-backup-scheduler.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Centralized Tool Access via Dashboard (Priority: P1)
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.
**Why this priority**: This is the core of the redesign, shifting the navigation paradigm to a dashboard-centric model for tools.
**Independent Test**: Can be tested by verifying the Dashboard contains links/cards for Backups, Mapper, and Storage, and that clicking them navigates to the correct full-page tools.
**Acceptance Scenarios**:
1. **Given** I am on the main Dashboard, **When** I look at the available tools, **Then** I see options for "Backup Manager", "Dataset Mapper", and "Storage Manager".
2. **Given** I am on the Dashboard, **When** I click "Backup Manager", **Then** I am taken to the full Backup management interface.
3. **Given** I am on the Dashboard, **When** I click "Dataset Mapper", **Then** I am taken to the Mapper tool.
4. **Given** I am on the Dashboard, **When** I click "Storage Manager", **Then** I am taken to the Storage tool.
---
### User Story 2 - Simplified Navigation Bar (Priority: P1)
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.
**Why this priority**: Enforces the separation of concerns between "Global Status/Settings" (Navbar) and "Operational Tools" (Dashboard).
**Independent Test**: Can be tested by inspecting the Navbar across the application to ensure removed items (Search, Mapper, Environments) are gone and only Tasks and Settings remain.
**Acceptance Scenarios**:
1. **Given** I am on any page, **When** I view the Navbar, **Then** I do NOT see links for "Dataset Search", "Dataset Mapper", or "Deployment Environments".
2. **Given** I am on any page, **When** I view the Navbar, **Then** I see links for "Tasks" and "Settings".
3. **Given** I am on the Tasks page, **When** I look for the "Run backup" button, **Then** it is NOT present (as it belongs in the Backup tool).
---
### User Story 3 - Deprecation of Unused Features (Priority: P2)
As a user, I want removed features (Dataset Search, Deployment Environments) to be inaccessible so that I don't use deprecated workflows.
**Why this priority**: Cleans up the UI and prevents confusion with features that are being removed or hidden.
**Independent Test**: Verify that UI elements for Dataset Search and Deployment Environments are removed from both Navbar and Dashboard.
**Acceptance Scenarios**:
1. **Given** I am on the Dashboard, **When** I look for "Dataset Search", **Then** it is not visible.
2. **Given** I am on the Dashboard or Navbar, **When** I look for "Deployment Environments", **Then** it is not visible.
### Edge Cases
- **Direct URL Access**: If a user attempts to access the URL of a removed page (e.g., `/search` or `/environments`) via bookmark or history, they should be redirected to the Dashboard or shown a 404 page (standard app behavior).
- **Mobile View**: The simplified Navbar must remain responsive; with fewer items, it should likely avoid collapsing into a hamburger menu unless necessary on very small screens.
### Assumptions
- The backend logic and UI components for the "Backup Scheduler" (Feature 009) are available and ready to be integrated into the main Dashboard view.
- Existing tools (Dataset Mapper, Storage Manager) function independently of the Navbar context and will work correctly when accessed via the Dashboard.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST display "Dataset Mapper" entry point on the main Dashboard.
- **FR-002**: System MUST display "Storage Manager" entry point on the main Dashboard.
- **FR-003**: System MUST display "Backup Scheduler" (or similar name) entry point on the main Dashboard.
- **FR-004**: System MUST provide access to the full Backup Management component (newly created based on feature 009 data) via the Dashboard link.
- **FR-005**: Navbar MUST NOT contain links to "Dataset Search", "Dataset Mapper", or "Deployment Environments".
- **FR-006**: Dashboard MUST NOT contain "Dataset Search" widget or link.
- **FR-007**: Tasks page MUST NOT show the "Run backup" button (backup initiation moves to Backup tool).
- **FR-008**: Navbar MUST retain "Tasks" and "Settings" links.
- **FR-009**: Backup Manager MUST support configuring automated backup schedules (enabled/disabled, cron expression) per environment.
- **FR-010**: Backup List MUST provide a "Go to Storage" action that navigates to the Storage Manager with the correct path selected.
### Key Entities
- **Dashboard**: The main landing page serving as the registry for tools.
- **Navbar**: The persistent top navigation for global application state/config.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Users can navigate to Dataset Mapper, Storage Manager, and Backup Manager within 1 click from the Dashboard.
- **SC-002**: Navbar contains strictly 0 links to operational tools (Mapper, Search, Storage), containing only Tasks and Settings.
- **SC-003**: "Run backup" action is successfully performed via the new Dashboard -> Backup route.

View File

@@ -0,0 +1,116 @@
# Tasks: Frontend Navigation Redesign
**Feature Branch**: `015-frontend-nav-redesign`
**Spec**: [specs/015-frontend-nav-redesign/spec.md](../spec.md)
**Plan**: [specs/015-frontend-nav-redesign/plan.md](../plan.md)
## Phase 1: Setup
*Goal: Initialize project structure for the new feature.*
- [x] T001 Create backup component directory structure
- Path: `frontend/src/components/backups/`
- [x] T002 Create backup page route directory
- Path: `frontend/src/routes/tools/backups/`
## Phase 2: Foundational
*Goal: Prepare core components for integration and verify backend connectivity.*
- [x] T003 Verify backend API endpoints for backups (via `009-backup-scheduler`)
- Path: `backend/src/api/routes/tasks.py` (or relevant backup route)
- [x] T004 Define Backup types and interfaces in frontend
- Path: `frontend/src/types/backup.ts`
## Phase 3: User Story 1 - Centralized Tool Access via Dashboard
*Goal: Update the main dashboard to include all tool entry points.*
**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)
- [x] T005 [US1] Update DashboardGrid to include "Backup Manager" card
- Path: `frontend/src/components/DashboardGrid.svelte`
- [x] T006 [US1] Update DashboardGrid to ensure "Dataset Mapper" and "Storage Manager" cards are present
- Path: `frontend/src/components/DashboardGrid.svelte`
- [x] T007 [US1] Remove "Dataset Search" card from DashboardGrid
- Path: `frontend/src/components/DashboardGrid.svelte`
## Phase 4: User Story 2 - Simplified Navigation Bar
*Goal: Clean up the Navbar to show only global context items.*
**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)
- [x] T008 [US2] Remove "Dataset Search" link from Navbar
- Path: `frontend/src/components/Navbar.svelte`
- [x] T009 [US2] Remove "Dataset Mapper" link from Navbar
- Path: `frontend/src/components/Navbar.svelte`
- [x] T010 [US2] Remove "Deployment Environments" link from Navbar
- Path: `frontend/src/components/Navbar.svelte`
- [x] T011 [US2] Verify "Tasks" and "Settings" links remain in Navbar
- Path: `frontend/src/components/Navbar.svelte`
- [x] T012 [US2] Remove "Run backup" button from Tasks page
- Path: `frontend/src/routes/tasks/+page.svelte` (or relevant component)
## Phase 5: Backup Management UI
*Goal: Implement the dedicated Backup Management interface.*
**User Story**: (Implicit P1 from FR-004) System MUST provide access to the full Backup Management component via the Dashboard link.
- [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`
- [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`
- [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`
- [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`
- [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`
- [x] T017b [US1] Implement Backup Schedule configuration in BackupManager
- Path: `frontend/src/components/backups/BackupManager.svelte`
- [x] T017c [US1] Implement "Go to Storage" navigation in BackupList
- Path: `frontend/src/components/backups/BackupList.svelte`
## Phase 6: User Story 3 - Deprecation
*Goal: Remove deprecated routes and code.*
**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)
- [x] T018 [US3] Delete Dataset Search route
- Path: `frontend/src/routes/tools/search/` (delete directory)
- [x] T019 [US3] Delete Deployment Environments route
- Path: `frontend/src/routes/settings/environments/` (delete directory)
- [x] T020 [US3] Delete Dataset Search component (if not used elsewhere)
- Path: `frontend/src/components/tools/SearchTool.svelte`
- [x] T021 [US3] Delete EnvSelector component (if not used elsewhere)
- Path: `frontend/src/components/EnvSelector.svelte`
## Phase 7: Polish & Cross-Cutting
*Goal: Final verification and cleanup.*
- [x] T022 Verify all navigation links work correctly
- Path: `frontend/src/components/Navbar.svelte`
- [x] T023 Verify responsive layout of new Dashboard grid
- Path: `frontend/src/components/DashboardGrid.svelte`
- [x] T024 Ensure i18n strings are extracted for new Backup UI
- Path: `frontend/src/lib/i18n/` (or relevant locale files)
- [x] T025 Verify "Run backup" action successfully triggers backup job (Manual/E2E check)
- Path: `frontend/src/components/backups/BackupManager.svelte`
## Dependencies
1. **Phase 1 & 2** must be completed first.
2. **Phase 3 (Dashboard)** and **Phase 4 (Navbar)** can be done in parallel.
3. **Phase 5 (Backup UI)** depends on Phase 1 & 2.
4. **Phase 6 (Deprecation)** should be done last to ensure no regressions before removal.
## Implementation Strategy
1. **Setup**: Create the new directory structure.
2. **Dashboard & Navbar**: Quick wins to reshape the navigation.
3. **Backup UI**: The core development effort. Connect to existing backend.
4. **Cleanup**: Remove old code once the new flows are verified.

View File

@@ -0,0 +1,34 @@
# Specification Quality Checklist: Multi-User Authentication and Authorization
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-01-26
**Feature**: [Link to spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`

View File

@@ -0,0 +1,39 @@
# Security Requirements Checklist: Multi-User Auth
**Purpose**: Validate completeness and rigor of security requirements for authentication and authorization.
**Created**: 2026-01-27
**Feature**: [Link to spec.md](../spec.md)
## Authentication Security
- [x] CHK001 Are password complexity requirements specified for local users? [Completeness, Gap] (Covered by T037)
- [x] CHK002 Is the exact hashing algorithm (bcrypt) and work factor specified? [Clarity, Spec §Research] (Covered by T006)
- [x] CHK003 Are account lockout policies defined for failed login attempts? [Coverage, Gap] (Covered by T033)
- [x] CHK004 Is the behavior for inactive/disabled accounts explicitly defined for both local and ADFS users? [Edge Case, Spec §Edge Cases] (Covered by T044)
- [x] CHK005 Are requirements defined for session revocation (e.g., logout, admin action)? [Completeness] (Covered by T043)
## ADFS & SSO Security
- [x] CHK006 Are token validation requirements (signature, issuer, audience) specified for ADFS OIDC tokens? [Completeness] (Covered by T007)
- [x] CHK007 Is the mapping behavior defined when an ADFS user is removed from a mapped AD group? [Edge Case, Gap] (Covered by T028)
- [x] CHK008 Are requirements defined for handling ADFS token expiration and refresh? [Coverage] (Covered by T046)
- [x] CHK009 Is the JIT provisioning process secure against privilege escalation (e.g., default role)? [Security, Spec §FR-008] (Covered by T028)
## Authorization & RBAC
- [x] CHK010 Are "default deny" requirements specified for plugin access? [Clarity, Spec §SC-002] (Covered by T020)
- [x] CHK011 Is the behavior defined when a user has multiple roles with conflicting permissions? [Edge Case, Gap] (Covered by T045)
- [x] CHK012 Are requirements specified for preventing admins from removing their own admin privileges (lockout prevention)? [Edge Case] (Covered by T022)
- [x] CHK013 Is the scope of "Execute" vs "Read" permission clearly defined for each plugin? [Clarity] (Covered by T019)
## Data Protection
- [x] CHK014 Are requirements defined for protecting sensitive data (passwords, tokens) in logs? [Completeness, Spec §Constitution] (Covered by T047)
- [x] CHK015 Are HttpOnly and Secure flags required for session cookies? [Clarity, Spec §Research] (Covered by T032)
- [x] CHK016 Is the storage mechanism for ADFS client secrets defined securely? [Completeness] (Covered by T002)
## API Security
- [x] CHK017 Are authentication requirements enforced on ALL API endpoints (except login)? [Coverage] (Covered by T021)
- [x] CHK018 Are rate limiting requirements defined for login endpoints to prevent brute force? [Gap] (Covered by T033)
- [x] CHK019 Are error messages required to be generic to avoid username enumeration? [Clarity] (Covered by T034)

View File

@@ -0,0 +1,31 @@
# Technical Readiness Checklist: Multi-User Auth
**Purpose**: Validate technical specifications, schema, and API contracts.
**Created**: 2026-01-27
**Feature**: [Link to spec.md](../spec.md)
## Data Model & Schema
- [x] CHK001 Are all necessary fields defined for the `User` entity (e.g., last_login)? [Completeness, Spec §Data Model] (Covered by T004)
- [x] CHK002 Are foreign key constraints explicitly defined for `ADGroupMapping`? [Clarity, Spec §Data Model] (Covered by T027)
- [x] CHK003 Is the uniqueness constraint for `username` and `email` specified? [Consistency] (Covered by T004)
- [x] CHK004 Are database migration requirements defined for the new `auth.db`? [Completeness, Gap] (Covered by T005)
## API Contracts
- [x] CHK005 Are request/response schemas defined for the `login` endpoint? [Completeness, Spec §Contracts] (Covered by T009)
- [x] CHK006 Are error response codes (401, 403, 404) standardized across all auth endpoints? [Consistency] (Covered by T012)
- [x] CHK007 Is the structure of the JWT payload (claims) explicitly defined? [Clarity, Spec §Research] (Covered by T007)
- [x] CHK008 Are pagination requirements defined for the "List Users" admin endpoint? [Gap] (Covered by T023)
## Dependencies & Integration
- [x] CHK009 Are version requirements specified for `Authlib` and `Passlib`? [Clarity, Spec §Plan] (Covered by T001)
- [x] CHK010 Is the dependency on the existing `TaskManager` for plugin execution defined? [Integration] (Covered by T021)
- [x] CHK011 Are requirements defined for the CLI admin creation tool? [Completeness, Spec §FR-009] (Covered by T008)
## Non-Functional Requirements
- [x] CHK012 Is the maximum acceptable latency for auth verification specified? [Clarity, Spec §Plan] (Covered by T013)
- [x] CHK013 Are concurrency requirements defined for the SQLite `auth.db` (WAL mode)? [Completeness, Spec §Research] (Covered by T003)
- [x] CHK014 Are logging requirements defined for audit trails (who did what)? [Completeness] (Covered by T047)

View File

@@ -0,0 +1,26 @@
# Testing Requirements Checklist: Multi-User Auth
**Purpose**: Validate test scenario coverage and strategy.
**Created**: 2026-01-27
**Feature**: [Link to spec.md](../spec.md)
## Functional Coverage
- [x] CHK001 Are positive test scenarios defined for Local Login? [Coverage, Spec §US-1] (Covered by T049)
- [x] CHK002 Are positive test scenarios defined for ADFS Login (mocked)? [Coverage, Spec §US-3] (Covered by T050)
- [x] CHK003 Are negative test scenarios defined for invalid passwords? [Coverage] (Covered by T049)
- [x] CHK004 Are negative test scenarios defined for unauthorized plugin access? [Coverage, Spec §US-2] (Covered by T049)
- [x] CHK005 Are test scenarios defined for switching between auth methods on the same screen? [Coverage] (Covered by T050)
## Edge Cases
- [x] CHK005 Are test scenarios defined for mixed-case username handling? [Edge Case] (Covered by T049)
- [x] CHK006 Are test scenarios defined for ADFS JIT provisioning with missing groups? [Edge Case] (Covered by T050)
- [x] CHK007 Are test scenarios defined for accessing the API with an expired token? [Edge Case] (Covered by T049)
- [x] CHK008 Are test scenarios defined for concurrent login sessions? [Edge Case] (Covered by T049)
## Integration & System
- [x] CHK009 Is the strategy defined for mocking ADFS during CI/CD tests? [Completeness] (Covered by T041)
- [x] CHK010 Are end-to-end tests required for the full admin user creation flow? [Coverage] (Covered by T050)
- [x] CHK011 Are tests required to verify the CLI admin creation tool? [Coverage] (Covered by T049)

View File

@@ -0,0 +1,31 @@
# UX Requirements Checklist: Multi-User Auth
**Purpose**: Validate user experience and interface requirements.
**Created**: 2026-01-27
**Feature**: [Link to spec.md](../spec.md)
## Login Flow
- [x] CHK001 Are feedback requirements defined for invalid credentials (generic message)? [Clarity, Spec §US-1] (Covered by T016)
- [x] CHK002 Is the redirect behavior specified after successful login (dashboard vs deep link)? [Clarity, Spec §US-1] (Covered by T016)
- [x] CHK003 Are loading states required during the ADFS redirection process? [Completeness] (Covered by T030)
- [x] CHK004 Is the "Session Expired" user flow defined? [Edge Case, Gap] (Covered by T035)
- [x] CHK005 Are requirements defined for the dual-mode login screen layout (Form + ADFS Button)? [Clarity, Spec §FR-013] (Covered by T030)
## Admin Interface
- [x] CHK005 Are requirements defined for the User Management list view (columns, sorting)? [Completeness] (Covered by T024)
- [x] CHK006 Is the feedback mechanism defined for successful/failed user creation? [Clarity] (Covered by T024)
- [x] CHK007 Are confirmation dialogs required for deleting users? [Safety, Gap] (Covered by T040)
- [x] CHK008 Is the UI behavior defined when assigning roles (dropdown, search)? [Clarity] (Covered by T024)
## Navigation & Visibility
- [x] CHK009 Are requirements defined for hiding menu items the user lacks permission for? [Completeness, Spec §FR-006] (Covered by T025)
- [x] CHK010 Is the behavior defined if a user tries to access a restricted URL directly? [Edge Case] (Covered by T042)
- [x] CHK011 Are user profile/logout controls required to be visible on all pages? [Consistency] (Covered by T025)
## Accessibility
- [x] CHK012 Are keyboard navigation requirements defined for the login form? [Coverage] (Covered by T048)
- [x] CHK013 Are error message accessibility requirements (ARIA alerts) specified? [Coverage] (Covered by T048)

View File

@@ -0,0 +1,132 @@
openapi: 3.0.0
info:
title: Authentication API
version: 1.0.0
paths:
/api/auth/login:
post:
summary: Login with username/password
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
username:
type: string
password:
type: string
responses:
'200':
description: Successful login
content:
application/json:
schema:
$ref: '#/components/schemas/Token'
'401':
description: Invalid credentials
/api/auth/login/adfs:
get:
summary: Initiate ADFS login flow
responses:
'302':
description: Redirect to ADFS provider
/api/auth/callback/adfs:
get:
summary: ADFS callback handler
parameters:
- in: query
name: code
schema:
type: string
required: true
responses:
'200':
description: Successful login via ADFS
content:
application/json:
schema:
$ref: '#/components/schemas/Token'
/api/auth/me:
get:
summary: Get current user profile
security:
- bearerAuth: []
responses:
'200':
description: User profile
content:
application/json:
schema:
$ref: '#/components/schemas/User'
/api/admin/users:
get:
summary: List all users
security:
- bearerAuth: []
responses:
'200':
description: List of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: Create a new user
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserCreate'
responses:
'201':
description: User created
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Token:
type: object
properties:
access_token:
type: string
token_type:
type: string
User:
type: object
properties:
id:
type: string
username:
type: string
email:
type: string
roles:
type: array
items:
type: string
UserCreate:
type: object
properties:
username:
type: string
password:
type: string
roles:
type: array
items:
type: string

View File

@@ -0,0 +1,86 @@
# Data Model: Multi-User Authentication
## Entities
### User
Represents an identity that can authenticate to the system.
| Field | Type | Description | Constraints |
|-------|------|-------------|-------------|
| `id` | UUID | Unique identifier | Primary Key |
| `username` | String | Unique login name | Unique, Not Null |
| `email` | String | User email address | Unique, Optional |
| `password_hash` | String | Bcrypt hash of password | Nullable (if ADFS) |
| `auth_source` | Enum | Source of identity | `LOCAL` or `ADFS` |
| `is_active` | Boolean | Account status | Default `True` |
| `created_at` | DateTime | Timestamp of creation | Auto-generated |
| `last_login` | DateTime | Timestamp of last login | Nullable |
### Role
Represents a collection of permissions.
| Field | Type | Description | Constraints |
|-------|------|-------------|-------------|
| `id` | UUID | Unique identifier | Primary Key |
| `name` | String | Human-readable role name | Unique, Not Null |
| `description` | String | Description of role purpose | Optional |
### Permission
Represents a specific capability within the system.
| Field | Type | Description | Constraints |
|-------|------|-------------|-------------|
| `id` | UUID | Unique identifier | Primary Key |
| `resource` | String | Target resource (e.g. `plugin:backup`) | Not Null |
| `action` | Enum | Type of access | `READ`, `EXECUTE`, `WRITE` |
### ADGroupMapping
Maps an Active Directory group to a local System Role.
| Field | Type | Description | Constraints |
|-------|------|-------------|-------------|
| `id` | UUID | Unique identifier | Primary Key |
| `ad_group_name` | String | Name of the group in AD | Unique, Not Null |
| `role_id` | UUID | ID of the local role to assign | Foreign Key -> Role.id |
## Relationships
- **User <-> Role**: Many-to-Many (via `user_roles` table)
- A User can have multiple Roles.
- A Role can be assigned to multiple Users.
- **Role <-> Permission**: Many-to-Many (via `role_permissions` table)
- A Role is defined by a set of Permissions.
- A Permission can belong to multiple Roles.
## Storage Schema (SQLAlchemy)
```python
# Conceptual Schema Definition
class User(Base):
__tablename__ = "users"
id = Column(String, primary_key=True, default=generate_uuid)
username = Column(String, unique=True, index=True, nullable=False)
password_hash = Column(String, nullable=True)
auth_source = Column(String, default="local")
is_active = Column(Boolean, default=True)
roles = relationship("Role", secondary="user_roles", back_populates="users")
class Role(Base):
__tablename__ = "roles"
id = Column(String, primary_key=True, default=generate_uuid)
name = Column(String, unique=True, nullable=False)
permissions = relationship("Permission", secondary="role_permissions")
users = relationship("User", secondary="user_roles", back_populates="roles")
class Permission(Base):
__tablename__ = "permissions"
id = Column(String, primary_key=True, default=generate_uuid)
resource = Column(String, nullable=False) # e.g., "plugin:backup"
action = Column(String, nullable=False) # e.g., "execute"
class ADGroupMapping(Base):
__tablename__ = "ad_group_mappings"
id = Column(String, primary_key=True, default=generate_uuid)
ad_group_name = Column(String, unique=True, nullable=False)
role_id = Column(String, ForeignKey("roles.id"), nullable=False)

View File

@@ -0,0 +1,98 @@
# Implementation Plan: Multi-User Authentication and Authorization
**Branch**: `016-multi-user-auth` | **Date**: 2026-01-26 | **Spec**: [`specs/016-multi-user-auth/spec.md`](spec.md)
**Input**: Feature specification from `specs/016-multi-user-auth/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
## Summary
Implement a robust authentication system supporting local users (username/password) and corporate SSO (ADFS via OIDC/OAuth2) simultaneously. The system will enforce Role-Based Access Control (RBAC) to restrict plugin access. Data will be persisted in a dedicated SQLite database (`auth.db`), and sessions will be managed via stateless JWTs. A CLI tool will be provided for initial admin provisioning. The login interface will provide dual options (Form + SSO Button) to ensure administrator access even during ADFS outages.
## Technical Context
**Language/Version**: Python 3.9+ (Backend), Node.js 18+ (Frontend)
**Primary Dependencies**:
- Backend: FastAPI, Authlib (ADFS/OIDC), Passlib[bcrypt] (Password hashing), PyJWT (Token management), SQLAlchemy (ORM for auth.db)
- Frontend: SvelteKit (UI), standard fetch API (JWT handling)
**Storage**: SQLite (`auth.db`) for Users, Roles, Permissions, and Mappings.
**Testing**: pytest (Backend), vitest/playwright (Frontend)
**Target Platform**: Linux server (Dockerized environment)
**Project Type**: Web Application (FastAPI Backend + SvelteKit Frontend)
**Performance Goals**: <100ms auth verification overhead per request.
**Constraints**: Must run in existing environment without external DB dependencies (hence SQLite).
**Scale/Scope**: ~10-100 concurrent users, ~5-10 distinct roles.
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- [x] **I. Semantic Protocol Compliance**: All new modules will use `[DEF]` anchors and `@RELATION` tags.
- [x] **II. Causal Validity**: Contracts (OpenAPI/Pydantic models) will be defined before implementation.
- [x] **III. Immutability of Architecture**: No changes to existing core architecture invariants; adding a new `AuthModule` layer.
- [x] **IV. Design by Contract**: All auth functions will define `@PRE`/`@POST` conditions.
- [x] **V. Belief State Logging**: Auth events will be logged using the standard belief scope logger.
- [x] **VI. Fractal Complexity Limit**: Auth logic will be modularized (Service, Repository, API layers).
- [x] **VII. Everything is a Plugin**: While core auth is middleware, the *management* of users/roles will be exposed via a System Plugin or dedicated Admin API, respecting the modular design.
- [x] **VIII. Unified Frontend Experience**: Login and Admin UI will use standard Svelte components and i18n.
## 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
backend/
├── src/
│ ├── api/
│ │ ├── auth/ # New: Auth endpoints (login, logout, refresh)
│ │ ├── admin/ # New: Admin endpoints (users, roles)
│ │ └── dependencies.py # Update: Add get_current_user, get_current_active_user
│ ├── core/
│ │ ├── auth/ # New: Core auth logic
│ │ │ ├── jwt.py # Token handling
│ │ │ ├── security.py # Password hashing
│ │ │ └── config.py # Auth settings
│ │ └── database.py # Update: Support for multiple DBs (auth.db)
│ ├── models/
│ │ └── auth.py # New: SQLAlchemy models (User, Role, Permission)
│ ├── schemas/ # New: Pydantic schemas for Auth
│ │ └── auth.py
│ └── services/
│ └── auth_service.py # New: Auth business logic
└── tests/
└── auth/ # New: Auth tests
frontend/
├── src/
│ ├── lib/
│ │ ├── auth/ # New: Frontend auth stores/logic
│ │ └── api/ # Update: Add auth headers to requests
│ ├── routes/
│ │ ├── login/ # New: Login page
│ │ └── admin/ # New: Admin dashboard (Users/Roles)
│ └── components/
│ └── auth/ # New: Auth components (ProtectedRoute, Login form)
```
**Structure Decision**: Web application structure with separated backend (FastAPI) and frontend (SvelteKit). Auth logic is centralized in `backend/src/core/auth` and `backend/src/services`, with a new persistent store `auth.db`. Frontend will implement a reactive auth store.
## 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,54 @@
# Quickstart: Multi-User Auth
## Prerequisites
- Python 3.9+
- Node.js 18+
- Existing project environment
## Setup
1. **Install Dependencies**:
```bash
pip install "passlib[bcrypt]" "python-jose[cryptography]" "Authlib" "sqlalchemy"
```
2. **Initialize Database**:
Run the migration script to create `auth.db` and tables.
```bash
python backend/src/scripts/init_auth_db.py
```
3. **Create Admin User**:
Use the CLI tool to create the initial superuser.
```bash
python backend/src/scripts/create_admin.py --username admin --password securepassword
```
## Running the Application
1. **Start Backend**:
```bash
cd backend
uvicorn src.app:app --reload
```
2. **Start Frontend**:
```bash
cd frontend
npm run dev
```
3. **Login**:
Navigate to `http://localhost:5173/login` and use the admin credentials created above.
## Configuring ADFS
1. Set environment variables in `.env`:
```ini
ADFS_CLIENT_ID=your-client-id
ADFS_CLIENT_SECRET=your-client-secret
ADFS_METADATA_URL=https://fs.your-company.com/adfs/.well-known/openid-configuration
```
2. Configure Group Mappings via the Admin UI or API.

View File

@@ -0,0 +1,76 @@
# Research: Multi-User Authentication and Authorization
## 1. Authentication Strategy
### Decision: Hybrid Local + ADFS (OIDC)
We will implement a dual authentication strategy:
1. **Local Auth**: Username/Password stored in `auth.db` with bcrypt hashing.
2. **ADFS**: OpenID Connect (OIDC) integration for enterprise SSO.
**Rationale**:
- **Local Auth**: Ensures the system is usable without external dependencies (ADFS) and provides a fallback for admins.
- **ADFS**: Requirement for corporate environment integration. OIDC is the modern standard supported by ADFS 2016+.
- **Just-In-Time (JIT)**: ADFS users will be provisioned locally upon first successful login if they belong to a mapped AD group.
**Alternatives Considered**:
- *SAML 2.0*: Older protocol, more complex to implement (XML-based) than OIDC. Rejected in favor of OIDC/OAuth2 support in `Authlib`.
- *LDAP Direct Bind*: Requires handling credentials directly, less secure than token-based SSO.
## 2. Session Management
### Decision: Stateless JWT (JSON Web Tokens)
Sessions will be managed using signed JWTs containing `sub` (user_id), `exp` (expiration), and `scopes` (roles).
**Rationale**:
- **Stateless**: No need to query the DB for every request to validate session validity (signature check is fast).
- **Scalable**: Works well with load balancers (though not a primary concern for this scale).
- **Frontend Friendly**: Easy to parse in JS to get user info without an extra API call.
**Security Measures**:
- Short-lived Access Tokens (e.g., 15-30 min).
- HttpOnly Cookies for storage to prevent XSS theft.
- Refresh Token rotation (stored in DB) for long-lived sessions.
## 3. Authorization Model
### Decision: RBAC (Role-Based Access Control)
Permissions are assigned to Roles. Users are assigned one or more Roles.
**Structure**:
- **Permissions**: Granular capabilities (e.g., `plugin:backup:execute`, `plugin:migration:read`).
- **Roles**: Collections of permissions (e.g., `Admin`, `Operator`, `Viewer`).
- **Users**: Assigned to Roles.
**Rationale**:
- Standard industry practice.
- Simplifies management: Admin assigns a role to a user rather than 50 individual permissions.
- AD Group Mapping fits naturally: `AD_Group_X` -> `Role_Y`.
## 4. Persistence
### Decision: Dedicated SQLite Database (`auth.db`)
A separate SQLite database file for authentication data.
**Rationale**:
- **Separation of Concerns**: Keeps auth data distinct from task history or other app data.
- **Relational Integrity**: Enforces foreign keys between Users, Roles, and Permissions better than JSON.
- **Concurrency**: SQLite WAL mode handles concurrent reads/writes better than a single JSON config file.
**Schema Draft**:
- `users` (id, username, password_hash, is_active, auth_source)
- `roles` (id, name, description)
- `permissions` (id, resource, action)
- `role_permissions` (role_id, permission_id)
- `user_roles` (user_id, role_id)
- `ad_group_mappings` (ad_group_name, role_id)
## 5. Frontend Integration
### Decision: SvelteKit Stores + HttpOnly Cookies
Authentication state will be synchronized between the server (cookies) and client (Svelte store).
**Mechanism**:
- Login endpoint sets `access_token` cookie (HttpOnly).
- Client makes API calls; browser automatically sends cookie.
- `hooks.server.ts` (or similar middleware) validates token on server-side rendering.
- Client-side store (`$auth`) holds user profile (decoded from token or fetched via `/me` endpoint) for UI logic (show/hide buttons).

View File

@@ -0,0 +1,113 @@
# Feature Specification: Multi-User Authentication and Authorization
**Feature Branch**: `016-multi-user-auth`
**Created**: 2026-01-26
**Status**: Draft
**Input**: User description: "Нужна поддержка многопользовательского логина. Нужно, чтобы пользователи могли логинится по связке логин/пароль, поддержка adfs, разделение прав доступа по плагинам"
## Clarifications
### Session 2026-01-26
- Q: Permission Model Structure? → A: RBAC (Role-Based Access Control) - Permissions assigned to Roles, Users assigned to Roles.
- Q: Initial Admin Provisioning? → A: CLI Command/Script - Explicit script to create the first admin user.
- Q: ADFS User Role Assignment? → A: AD Group Mapping - Login requires valid AD group membership; AD groups map to local Roles (e.g., 'superset_admin' -> 'Admin').
- Q: Token Management? → A: JWT (JSON Web Tokens) - Stateless, scalable, standard for SPAs.
- Q: Persistence Layer? → A: Dedicated SQLite DB (`auth.db`) - Relational storage for Users, Roles, Permissions.
- Q: Switching Auth Providers? → A: Dual Support - Both Local and ADFS login options are available simultaneously on the login page.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Local User Authentication (Priority: P1)
As a user, I want to log in using a username and password so that I can securely access the application.
**Why this priority**: Basic authentication is the foundation for multi-user support and is required before implementing more complex auth methods or permissions.
**Independent Test**: Can be fully tested by creating a local user account and successfully logging in/out without any external dependencies.
**Acceptance Scenarios**:
1. **Given** a registered user, **When** they enter valid credentials on the login page, **Then** they are redirected to the dashboard and receive a session token.
2. **Given** a registered user, **When** they enter invalid credentials, **Then** they see an error message "Invalid username or password".
3. **Given** an authenticated user, **When** they click logout, **Then** their session is terminated and they are redirected to the login page.
---
### User Story 2 - Plugin-Based Access Control (Priority: P1)
As an administrator, I want to assign specific plugin access rights to users so that I can control who can use sensitive tools (e.g., Backup, Migration).
**Why this priority**: Security is a core requirement. Without granular permissions, all authenticated users would have full administrative access, which defeats the purpose of multi-user support.
**Independent Test**: Create two users with different permissions (e.g., User A has access to "Backup", User B does not). Verify User A can access the Backup tool while User B receives a 403 Forbidden error.
**Acceptance Scenarios**:
1. **Given** a user with "Backup" plugin permission, **When** they navigate to the Backup tool, **Then** the page loads successfully.
2. **Given** a user WITHOUT "Backup" plugin permission, **When** they navigate to the Backup tool, **Then** they are denied access (UI hides the link, API returns 403).
3. **Given** an administrator, **When** they edit a user's permissions, **Then** the changes take effect immediately or upon next login.
---
### User Story 3 - ADFS Integration (Priority: P2)
As a corporate user, I want to log in using my organization's ADFS credentials so that I don't have to manage a separate password.
**Why this priority**: Essential for enterprise environments but dependent on the core authentication infrastructure being in place (Story 1).
**Independent Test**: Configure the application with a test ADFS provider (or mock). Verify a user can initiate the SSO flow and be logged in automatically.
**Acceptance Scenarios**:
1. **Given** a configured ADFS provider, **When** a user clicks "Login with ADFS", **Then** they are redirected to the identity provider.
2. **Given** a successful ADFS authentication, **When** the user returns to the app, **Then** a local user session is created/matched and they are logged in.
3. **Given** a new ADFS user, **When** they log in for the first time, **Then** a local user record is automatically created (JIT provisioning) with default permissions.
---
### Edge Cases
- What happens when an ADFS user's account is disabled in the local system? (Should block login even if ADFS succeeds)
- How does the system handle concurrent sessions? (Allow or restrict?)
- What happens if a plugin is removed but users still have permission for it? (Graceful handling/cleanup)
- What happens if the ADFS server is unreachable? (Fallback to local login if applicable, or clear error message)
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST support local user authentication via username and password.
- **FR-002**: System MUST support authentication via ADFS (Active Directory Federation Services) using standard federation protocols.
- **FR-003**: System MUST provide a mechanism to manage users (Create, Read, Update, Delete) - restricted to administrators.
- **FR-004**: System MUST implement Role-Based Access Control (RBAC) where permissions are assigned to Roles, and Roles are assigned to Users.
- **FR-005**: System MUST enforce permissions at the server level for all plugin execution requests.
- **FR-006**: System MUST enforce permissions at the user interface level (hide navigation items/buttons for unauthorized plugins).
- **FR-007**: System MUST securely store local user credentials.
- **FR-008**: System MUST support Just-In-Time (JIT) provisioning for ADFS users ONLY if they belong to a mapped AD group.
- **FR-009**: System MUST provide a CLI utility to create an initial administrator account to prevent lockout during first deployment.
- **FR-010**: System MUST allow configuring mappings between Active Directory Groups and local System Roles.
- **FR-011**: System MUST use JWT (JSON Web Tokens) for API session management.
- **FR-012**: System MUST persist authentication and authorization data in a dedicated SQLite database (`auth.db`).
- **FR-013**: System MUST provide a unified login interface supporting both Local (Username/Password) and ADFS (SSO Button) authentication methods simultaneously.
### Key Entities
- **User**: Represents a system user. Attributes: ID, Username, Email, PasswordHash, AuthSource (Local/ADFS), IsActive, Roles (List[RoleID]).
- **Role**: Named collection of permissions. Attributes: ID, Name, Description, Permissions (List[Permission]).
- **Permission**: Represents access capability. Attributes: ResourceID (e.g., Plugin ID), Action (Execute, Read).
- **ADGroupMapping**: Configuration mapping AD Group names to Role IDs.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Administrators can successfully create a new local user and assign specific plugin permissions in under 2 minutes.
- **SC-002**: Users without permission for a specific plugin are denied access 100% of the time when attempting to use its functions.
- **SC-003**: ADFS login flow completes successfully for valid credentials and maps to the correct local user identity.
- **SC-004**: User interface dynamically updates to show only permitted tools for the logged-in user.
## Assumptions
- The application currently has a simple or placeholder authentication mechanism.
- "Plugin access" refers to the ability to use the plugin's functionality and view its interface.
- A default administrator account will be available upon initial system setup to prevent lockout.

View File

@@ -0,0 +1,91 @@
# Tasks: Multi-User Authentication and Authorization
**Feature Branch**: `016-multi-user-auth`
**Feature Spec**: [`specs/016-multi-user-auth/spec.md`](spec.md)
**Implementation Plan**: [`specs/016-multi-user-auth/plan.md`](plan.md)
## Phase 1: Setup & Infrastructure (Blocking)
*Goal: Initialize the auth database, core dependencies, and backend infrastructure.*
- [ ] T001 Install backend dependencies (Authlib, Passlib, PyJWT, SQLAlchemy) in `backend/requirements.txt`
- [ ] T002 Implement core configuration for Auth and Database in `backend/src/core/auth/config.py`
- [ ] T003 Implement database connection logic for `auth.db` in `backend/src/core/database.py`
- [ ] T004 Create SQLAlchemy models for User, Role, Permission in `backend/src/models/auth.py`
- [ ] T005 Create migration/init script to generate `auth.db` schema in `backend/src/scripts/init_auth_db.py`
- [ ] T006 Implement password hashing utility using Passlib in `backend/src/core/auth/security.py`
- [ ] T007 Implement JWT token generation and validation logic in `backend/src/core/auth/jwt.py`
- [ ] T008 [P] Implement CLI tool for creating the initial admin user in `backend/src/scripts/create_admin.py`
## Phase 2: User Story 1 - Local User Authentication (Priority: P1)
*Goal: Enable users to log in with username/password and receive a JWT session.*
- [ ] T009 [US1] Create Pydantic schemas for User, UserCreate, Token in `backend/src/schemas/auth.py`
- [ ] T010 [US1] Implement `AuthRepository` for DB operations in `backend/src/core/auth/repository.py`
- [ ] T011 [US1] Implement `AuthService` for login logic (verify password, create token) in `backend/src/services/auth_service.py`
- [ ] T012 [US1] Create API endpoint `POST /api/auth/login` in `backend/src/api/auth.py`
- [ ] T013 [US1] Implement `get_current_user` dependency for JWT verification in `backend/src/dependencies.py`
- [ ] T014 [US1] Create API endpoint `GET /api/auth/me` to retrieve current user profile in `backend/src/api/auth.py`
- [ ] T043 [US1] Implement session revocation (Logout) endpoint in `backend/src/api/auth.py`
- [ ] T044 [US1] Implement account status check (`is_active`) in authentication flow in `backend/src/services/auth_service.py`
- [ ] T015 [US1] Implement frontend auth store (Svelte store) in `frontend/src/lib/auth/store.ts`
- [ ] T016 [US1] Implement Login Page UI using `src/lib/ui` and `src/lib/i18n` in `frontend/src/routes/login/+page.svelte`
- [ ] T017 [US1] Integrate Login Page with Backend API in `frontend/src/routes/login/+page.svelte`
- [ ] T018 [US1] Implement `ProtectedRoute` component to redirect unauthenticated users in `frontend/src/components/auth/ProtectedRoute.svelte`
- [ ] T037 [US1] Implement password complexity validation logic in `backend/src/core/auth/security.py`
## Phase 3: User Story 2 - Plugin-Based Access Control (Priority: P1)
*Goal: Restrict access to plugins based on user roles and permissions.*
- [ ] T019 [US2] Update `PluginBase` to include required permission strings in `backend/src/core/plugin_base.py`
- [ ] T020 [US2] Implement `has_permission` dependency for route protection in `backend/src/dependencies.py`
- [ ] T021 [US2] Protect existing plugin API routes using `has_permission` in `backend/src/api/routes/*.py`
- [ ] T022 [US2] Implement `SystemAdminPlugin` inheriting from `PluginBase` for User/Role management in `backend/src/plugins/system_admin.py`
- [ ] T023 [US2] Implement Admin API endpoints within `SystemAdminPlugin` (with pagination) in `backend/src/api/routes/admin.py`
- [ ] T024 [US2] Create Admin Dashboard UI using `src/lib/ui` and `src/lib/i18n` in `frontend/src/routes/admin/users/+page.svelte`
- [ ] T025 [US2] Update Navigation Bar to hide links and show user profile/logout using `src/lib/ui` in `frontend/src/components/Navbar.svelte`
- [ ] T042 [US2] Implement `PermissionGuard` frontend component for granular UI element protection in `frontend/src/components/auth/PermissionGuard.svelte`
- [ ] T045 [US2] Implement multi-role permission resolution logic (union of permissions) in `backend/src/services/auth_service.py`
## Phase 4: User Story 3 - ADFS Integration (Priority: P2)
*Goal: Enable corporate SSO login via ADFS and JIT provisioning.*
- [ ] T026 [US3] Configure Authlib for ADFS OIDC in `backend/src/core/auth/oauth.py`
- [ ] T027 [US3] Create `ADGroupMapping` model in `backend/src/models/auth.py` and update DB init script
- [ ] T028 [US3] Implement JIT provisioning logic (create user if maps to group) in `backend/src/services/auth_service.py`
- [ ] T029 [US3] Create API endpoints `GET /api/auth/login/adfs` and `GET /api/auth/callback/adfs` in `backend/src/api/auth.py`
- [ ] T030 [US3] Update Login Page to include "Login with ADFS" button using `src/lib/ui` in `frontend/src/routes/login/+page.svelte`
- [ ] T031 [US3] Implement Admin UI for configuring AD Group Mappings in `frontend/src/routes/admin/settings/+page.svelte`
- [ ] T041 [US3] Create ADFS mock provider for local testing and CI in `backend/tests/auth/mock_adfs.py`
- [ ] T046 [US3] Implement token refresh logic for ADFS OIDC tokens in `backend/src/core/auth/jwt.py`
## Phase 5: Polish & Security Hardening
*Goal: Ensure security best practices and smooth UX.*
- [ ] T032 Ensure all cookies are set with `HttpOnly` and `Secure` flags in `backend/src/api/auth.py`
- [ ] T033 Implement rate limiting and account lockout policy in `backend/src/api/auth.py`
- [ ] T034 Verify error messages are generic (no username enumeration) across all auth endpoints
- [ ] T035 Add "Session Expired" handling in frontend interceptor in `frontend/src/lib/api/client.ts`
- [ ] T036 Final manual test of switching between Local and ADFS login flows
- [ ] T040 Add confirmation dialogs for destructive admin actions using `src/lib/ui` in `frontend/src/routes/admin/users/+page.svelte`
- [ ] T047 Implement audit logging for security events (login, logout, permission changes) in `backend/src/core/auth/logger.py`
- [ ] T048 Perform UI accessibility audit (keyboard nav, ARIA alerts) for all auth components
- [ ] T049 Implement unit and integration tests for Local Auth and RBAC in `backend/tests/auth/`
- [ ] T050 Implement E2E tests for ADFS flow using mock provider in `tests/e2e/auth.spec.ts`
## Dependencies
1. **Phase 1** must be completed before any User Stories.
2. **Phase 2 (Local Auth)** is the foundation for authentication and session management.
3. **Phase 3 (RBAC)** depends on Phase 2 (needs authenticated users to check permissions).
4. **Phase 4 (ADFS)** depends on Phase 2 (uses same session mechanism) and Phase 3 (needs roles for JIT).
## Implementation Strategy
- **MVP**: Complete Phases 1 and 2. This gives a working auth system with local users.
- **Increment 1**: Complete Phase 3. This adds the critical security controls (RBAC).
- **Increment 2**: Complete Phase 4. This adds corporate SSO convenience.