kin: auto-commit after pipeline

This commit is contained in:
Gros Frumos 2026-03-17 17:39:40 +02:00
parent 248934d5d7
commit 939a30a3de
6 changed files with 796 additions and 3 deletions

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { api, ApiError, type ProjectDetail, type AuditResult, type Phase, type Task, type ProjectEnvironment } from '../api'
import { api, ApiError, type ProjectDetail, type AuditResult, type Phase, type Task, type ProjectEnvironment, type DeployResult, type ProjectLink } from '../api'
import Badge from '../components/Badge.vue'
import Modal from '../components/Modal.vue'
@ -12,7 +12,7 @@ const router = useRouter()
const project = ref<ProjectDetail | null>(null)
const loading = ref(true)
const error = ref('')
const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments'>('tasks')
const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments' | 'links'>('tasks')
// Phases
const phases = ref<Phase[]>([])
@ -369,6 +369,81 @@ async function deleteEnv(envId: number) {
}
}
// Deploy
const deploying = ref(false)
const deployResult = ref<DeployResult | null>(null)
const hasDeployConfig = computed(() => {
if (!project.value) return false
return !!(project.value.deploy_host && project.value.deploy_path && project.value.deploy_runtime) || !!project.value.deploy_command
})
async function runDeploy() {
deploying.value = true
deployResult.value = null
try {
deployResult.value = await api.deployProject(props.id)
} catch (e: any) {
error.value = e.message
} finally {
deploying.value = false
}
}
// Project Links
const links = ref<ProjectLink[]>([])
const linksLoading = ref(false)
const linksError = ref('')
const showAddLink = ref(false)
const linkForm = ref({ to_project: '', link_type: 'depends_on', description: '' })
const linkFormError = ref('')
const linkSaving = ref(false)
async function loadLinks() {
linksLoading.value = true
linksError.value = ''
try {
links.value = await api.projectLinks(props.id)
} catch (e: any) {
linksError.value = e.message
} finally {
linksLoading.value = false
}
}
async function addLink() {
linkFormError.value = ''
if (!linkForm.value.to_project) { linkFormError.value = 'Выберите проект'; return }
linkSaving.value = true
try {
await api.createProjectLink({
from_project: props.id,
to_project: linkForm.value.to_project,
link_type: linkForm.value.link_type,
description: linkForm.value.description || undefined,
})
showAddLink.value = false
linkForm.value = { to_project: '', link_type: 'depends_on', description: '' }
await loadLinks()
} catch (e: any) {
linkFormError.value = e.message
} finally {
linkSaving.value = false
}
}
async function deleteLink(id: number) {
if (!confirm('Удалить связь?')) return
try {
await api.deleteProjectLink(id)
await loadLinks()
} catch (e: any) {
linksError.value = e.message
}
}
const allProjects = ref<{ id: string; name: string }[]>([])
// Add task modal
const TASK_CATEGORIES = ['SEC', 'UI', 'API', 'INFRA', 'BIZ', 'DB', 'ARCH', 'TEST', 'PERF', 'DOCS', 'FIX', 'OBS']
const CATEGORY_COLORS: Record<string, string> = {
@ -425,12 +500,19 @@ watch(() => props.id, () => {
environments.value = []
showScanBanner.value = false
scanTaskId.value = null
links.value = []
deployResult.value = null
})
onMounted(async () => {
await load()
await loadPhases()
await loadEnvironments()
await loadLinks()
try {
const all = await api.projects()
allProjects.value = all.map(p => ({ id: p.id, name: p.name }))
} catch {}
})
onUnmounted(() => {

View file

@ -19,6 +19,14 @@ const saveAutoTestStatus = ref<Record<string, string>>({})
const syncResults = ref<Record<string, ObsidianSyncResult | null>>({})
const error = ref<string | null>(null)
// Deploy config
const deployHosts = ref<Record<string, string>>({})
const deployPaths = ref<Record<string, string>>({})
const deployRuntimes = ref<Record<string, string>>({})
const deployRestartCmds = ref<Record<string, string>>({})
const savingDeployConfig = ref<Record<string, boolean>>({})
const saveDeployConfigStatus = ref<Record<string, string>>({})
onMounted(async () => {
try {
projects.value = await api.projects()
@ -27,12 +35,34 @@ onMounted(async () => {
deployCommands.value[p.id] = p.deploy_command ?? ''
testCommands.value[p.id] = p.test_command ?? ''
autoTestEnabled.value[p.id] = !!(p.auto_test_enabled)
deployHosts.value[p.id] = p.deploy_host ?? ''
deployPaths.value[p.id] = p.deploy_path ?? ''
deployRuntimes.value[p.id] = p.deploy_runtime ?? ''
deployRestartCmds.value[p.id] = p.deploy_restart_cmd ?? ''
}
} catch (e) {
error.value = String(e)
}
})
async function saveDeployConfig(projectId: string) {
savingDeployConfig.value[projectId] = true
saveDeployConfigStatus.value[projectId] = ''
try {
await api.patchProject(projectId, {
deploy_host: deployHosts.value[projectId] || undefined,
deploy_path: deployPaths.value[projectId] || undefined,
deploy_runtime: deployRuntimes.value[projectId] || undefined,
deploy_restart_cmd: deployRestartCmds.value[projectId] || undefined,
})
saveDeployConfigStatus.value[projectId] = 'Saved'
} catch (e) {
saveDeployConfigStatus.value[projectId] = `Error: ${e}`
} finally {
savingDeployConfig.value[projectId] = false
}
}
async function saveVaultPath(projectId: string) {
saving.value[projectId] = true
saveStatus.value[projectId] = ''
@ -172,6 +202,63 @@ async function runSync(projectId: string) {
</span>
</div>
<!-- Deploy Config -->
<div class="mb-2 pt-2 border-t border-gray-800">
<p class="text-xs font-semibold text-gray-400 mb-2">Deploy Config</p>
<div class="mb-2">
<label class="block text-xs text-gray-500 mb-1">Server host</label>
<input
v-model="deployHosts[project.id]"
type="text"
placeholder="server host (e.g. vdp-prod)"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500"
/>
</div>
<div class="mb-2">
<label class="block text-xs text-gray-500 mb-1">Project path on server</label>
<input
v-model="deployPaths[project.id]"
type="text"
placeholder="/srv/myproject"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500"
/>
</div>
<div class="mb-2">
<label class="block text-xs text-gray-500 mb-1">Runtime</label>
<select
v-model="deployRuntimes[project.id]"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300 focus:outline-none focus:border-gray-500"
>
<option value=""> выберите runtime </option>
<option value="docker">docker</option>
<option value="node">node</option>
<option value="python">python</option>
<option value="static">static</option>
</select>
</div>
<div class="mb-2">
<label class="block text-xs text-gray-500 mb-1">Restart command (optional override)</label>
<input
v-model="deployRestartCmds[project.id]"
type="text"
placeholder="optional override command"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500"
/>
</div>
<div class="flex items-center gap-3 flex-wrap mb-3">
<button
@click="saveDeployConfig(project.id)"
:disabled="savingDeployConfig[project.id]"
class="px-3 py-1.5 text-sm bg-teal-900/50 text-teal-400 border border-teal-800 rounded hover:bg-teal-900 disabled:opacity-50"
>
{{ savingDeployConfig[project.id] ? 'Saving…' : 'Save Deploy Config' }}
</button>
<span v-if="saveDeployConfigStatus[project.id]" class="text-xs" :class="saveDeployConfigStatus[project.id].startsWith('Error') ? 'text-red-400' : 'text-green-400'">
{{ saveDeployConfigStatus[project.id] }}
</span>
</div>
</div>
<div class="flex items-center gap-3 mb-3">
<label class="flex items-center gap-2 cursor-pointer select-none">
<input

View file

@ -6,6 +6,7 @@ import Badge from '../components/Badge.vue'
import Modal from '../components/Modal.vue'
import AttachmentUploader from '../components/AttachmentUploader.vue'
import AttachmentList from '../components/AttachmentList.vue'
import LiveConsole from '../components/LiveConsole.vue'
const props = defineProps<{ id: string }>()
const route = useRoute()
@ -468,6 +469,14 @@ async function saveEdit() {
No pipeline steps yet.
</div>
<!-- Live Console -->
<LiveConsole
v-if="task.pipeline_id"
:pipeline-id="task.pipeline_id"
:pipeline-status="task.status"
class="mb-6"
/>
<!-- Selected step output -->
<div v-if="selectedStep" class="mb-6">
<h2 class="text-sm font-semibold text-gray-300 mb-2">