feat(git): implement deployment tracking and enhance lifecycle UX

Introduce a deployment recording system to track dashboard versions
across environments and improve the Git management user experience.

- Add `Deployment` model and Alembic migration to persist deployment
  history.
- Implement `GitDeploymentRecorder` and `GitFingerprint` plugins to
  automate deployment logging and content hashing.
- Add `get_deployment_status` API endpoint to retrieve real-time
  environment states.
- Refactor `GitLifecycleHeader` to prioritize Call-to-Action (CTA)
  buttons and improve visual hierarchy.
- Update `GitWorkspacePanel` to emphasize version saving and
  streamline commit workflows.
- Enhance `GitEnvironmentTimeline` with deployment status integration,
  collapsible UI, and improved theme consistency.
- Add auto-navigation logic in `GitManagerModel` to guide users to
  relevant tabs based on recommended actions.
- Clean up obsolete documentation and update i18n strings for
  git visualization features.
This commit is contained in:
2026-07-12 14:57:03 +03:00
parent b39d9991b9
commit 0cb1f80cd6
34 changed files with 2018 additions and 1512 deletions

View File

@@ -91,24 +91,39 @@
function handleRecommendedAction() {
if (model.recommendedAction === 'sync') {
void model.handleSync();
model.autoNavigateTab = 'workspace';
model.activeTab = 'workspace';
void model.handleSync();
return;
}
if (model.recommendedAction === 'commit') {
model.autoNavigateTab = 'workspace';
model.activeTab = 'workspace';
return;
}
if (model.recommendedAction === 'promote') {
model.autoNavigateTab = 'release';
model.activeTab = 'release';
return;
}
model.autoNavigateTab = 'operations';
model.activeTab = 'operations';
}
$effect(() => {
if (model.showDeployConfirm) deployConfirmInput = '';
});
// ── Auto-navigate tab pulse indicator ──
let pulseTab: string | null = $state(null);
$effect(() => {
if (model.autoNavigateTab) {
pulseTab = model.autoNavigateTab;
model.autoNavigateTab = null;
const timer = setTimeout(() => { pulseTab = null; }, 2000);
return () => clearTimeout(timer);
}
});
function handleBackdropClick(e) { if (e.target === e.currentTarget) closeModal(); }
onMount(() => {
@@ -151,7 +166,7 @@
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium text-text-muted transition-colors hover:bg-surface-muted hover:text-text disabled:opacity-50"
aria-label={$t.common?.refresh || 'Refresh'}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 {model.checkingStatus ? 'animate-spin' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M16.023 9.348H20M14.985 19.644H20M2.985 14.652V20" /></svg>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="h-3.5 w-3.5 {model.checkingStatus ? 'animate-spin' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M16.023 9.348H20M14.985 19.644H20M2.985 14.652V20" /></svg>
{$t.common?.refresh || 'Refresh'}
</button>
<button type="button" onclick={closeModal} class="flex h-9 w-9 items-center justify-center rounded-lg text-text-subtle transition-colors hover:bg-surface-muted hover:text-text" aria-label={$t.common?.close || 'Close'}>
@@ -254,7 +269,7 @@
<!-- Branch selector moved here per UX review: separate from version map to avoid confusion -->
<div class="flex items-center gap-3 mb-2">
<div class="text-xs font-medium text-text-muted uppercase tracking-wide">Рабочая ветка (текущие изменения):</div>
<div class="text-xs font-medium text-text-muted uppercase tracking-wide">{$t.git?.working_branch || 'Working branch (current changes):'}</div>
<div class="flex-1 max-w-[280px]">
<BranchSelector
{dashboardId}
@@ -271,6 +286,7 @@
branches={model.branches}
environmentHistories={model.environmentHistories}
historiesLoading={model.environmentHistoriesLoading}
deploymentStatus={model.deploymentStatus}
{dashboardId}
envId={model.resolvedEnvId}
bind:currentBranch={model.currentBranch}
@@ -291,17 +307,17 @@
/>
<div class="flex flex-wrap items-center gap-1 border-b border-border pb-0" role="tablist" aria-label={$t.git?.management || 'Управление Git'}>
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors ${model.activeTab === 'workspace' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'}`} onclick={() => (model.activeTab = 'workspace')} role="tab" aria-selected={model.activeTab === 'workspace'} aria-controls="git-tab-panel" id="git-tab-workspace" tabindex={model.activeTab === 'workspace' ? 0 : -1}>
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-primary-ring ${model.activeTab === 'workspace' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'} ${pulseTab === 'workspace' ? 'animate-pulse ring-2 ring-primary-ring' : ''}`} onclick={() => (model.activeTab = 'workspace')} role="tab" aria-selected={model.activeTab === 'workspace'} aria-controls="git-tab-panel" id="git-tab-workspace" tabindex={model.activeTab === 'workspace' ? 0 : -1}>
<Icon name="edit" size={16} strokeWidth={2} />
{$t.git?.tab_workspace || 'Изменения'}
<HelpTooltip text={$t.git?.hint_workspace || ''} ariaLabel={$t.git?.help_aria || 'Справка'} />
</button>
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors ${model.activeTab === 'release' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'}`} onclick={() => (model.activeTab = 'release')} role="tab" aria-selected={model.activeTab === 'release'} aria-controls="git-tab-panel" id="git-tab-release" tabindex={model.activeTab === 'release' ? 0 : -1}>
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-primary-ring ${model.activeTab === 'release' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'} ${pulseTab === 'release' ? 'animate-pulse ring-2 ring-primary-ring' : ''}`} onclick={() => (model.activeTab = 'release')} role="tab" aria-selected={model.activeTab === 'release'} aria-controls="git-tab-panel" id="git-tab-release" tabindex={model.activeTab === 'release' ? 0 : -1}>
<Icon name="lightning" size={16} strokeWidth={2} />
{$t.git?.tab_release || 'Релиз'}
<HelpTooltip text={$t.git?.hint_release || ''} ariaLabel={$t.git?.help_aria || 'Справка'} />
</button>
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors ${model.activeTab === 'operations' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'}`} onclick={() => (model.activeTab = 'operations')} role="tab" aria-selected={model.activeTab === 'operations'} aria-controls="git-tab-panel" id="git-tab-operations" tabindex={model.activeTab === 'operations' ? 0 : -1}>
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-primary-ring ${model.activeTab === 'operations' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'} ${pulseTab === 'operations' ? 'animate-pulse ring-2 ring-primary-ring' : ''}`} onclick={() => (model.activeTab = 'operations')} role="tab" aria-selected={model.activeTab === 'operations'} aria-controls="git-tab-panel" id="git-tab-operations" tabindex={model.activeTab === 'operations' ? 0 : -1}>
<Icon name="settings" size={16} strokeWidth={2} />
{$t.git?.tab_operations || 'Серверные операции'}
<HelpTooltip text={$t.git?.hint_operations || ''} ariaLabel={$t.git?.help_aria || 'Справка'} />