kin: KIN-BIZ-006 Проверить промпт sysadmin.md на поддержку сценария env_scan
This commit is contained in:
parent
531275e4ce
commit
a58578bb9d
14 changed files with 1619 additions and 13 deletions
|
|
@ -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 } from '../api'
|
||||
import { api, ApiError, type ProjectDetail, type AuditResult, type Phase, type Task, type ProjectEnvironment } 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'>('tasks')
|
||||
const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments'>('tasks')
|
||||
|
||||
// Phases
|
||||
const phases = ref<Phase[]>([])
|
||||
|
|
@ -246,6 +246,83 @@ async function applyAudit() {
|
|||
}
|
||||
}
|
||||
|
||||
// Environments
|
||||
const environments = ref<ProjectEnvironment[]>([])
|
||||
const envsLoading = ref(false)
|
||||
const envsError = ref('')
|
||||
const showEnvModal = ref(false)
|
||||
const editingEnv = ref<ProjectEnvironment | null>(null)
|
||||
const envForm = ref({ name: 'prod', host: '', port: 22, username: '', auth_type: 'password', auth_value: '', is_installed: false })
|
||||
const envFormError = ref('')
|
||||
const envSaving = ref(false)
|
||||
const scanTaskId = ref<string | null>(null)
|
||||
const showScanBanner = ref(false)
|
||||
|
||||
async function loadEnvironments() {
|
||||
envsLoading.value = true
|
||||
envsError.value = ''
|
||||
try {
|
||||
environments.value = await api.environments(props.id)
|
||||
} catch (e: any) {
|
||||
envsError.value = e.message
|
||||
} finally {
|
||||
envsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openEnvModal(env?: ProjectEnvironment) {
|
||||
editingEnv.value = env || null
|
||||
if (env) {
|
||||
envForm.value = { name: env.name, host: env.host, port: env.port, username: env.username, auth_type: env.auth_type, auth_value: '', is_installed: !!env.is_installed }
|
||||
} else {
|
||||
envForm.value = { name: 'prod', host: '', port: 22, username: '', auth_type: 'password', auth_value: '', is_installed: false }
|
||||
}
|
||||
envFormError.value = ''
|
||||
showEnvModal.value = true
|
||||
}
|
||||
|
||||
async function submitEnv() {
|
||||
envFormError.value = ''
|
||||
envSaving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
name: envForm.value.name,
|
||||
host: envForm.value.host,
|
||||
port: envForm.value.port,
|
||||
username: envForm.value.username,
|
||||
auth_type: envForm.value.auth_type,
|
||||
auth_value: envForm.value.auth_value || undefined,
|
||||
is_installed: envForm.value.is_installed,
|
||||
}
|
||||
let res: ProjectEnvironment & { scan_task_id?: string }
|
||||
if (editingEnv.value) {
|
||||
res = await api.updateEnvironment(props.id, editingEnv.value.id, payload)
|
||||
} else {
|
||||
res = await api.createEnvironment(props.id, payload)
|
||||
}
|
||||
showEnvModal.value = false
|
||||
await loadEnvironments()
|
||||
if (res.scan_task_id) {
|
||||
scanTaskId.value = res.scan_task_id
|
||||
showScanBanner.value = true
|
||||
}
|
||||
} catch (e: any) {
|
||||
envFormError.value = e.message
|
||||
} finally {
|
||||
envSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEnv(envId: number) {
|
||||
if (!confirm('Удалить среду?')) return
|
||||
try {
|
||||
await api.deleteEnvironment(props.id, envId)
|
||||
await loadEnvironments()
|
||||
} catch (e: any) {
|
||||
envsError.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
// Add task modal
|
||||
const TASK_CATEGORIES = ['SEC', 'UI', 'API', 'INFRA', 'BIZ', 'DB', 'ARCH', 'TEST', 'PERF', 'DOCS', 'FIX', 'OBS']
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
|
|
@ -280,6 +357,9 @@ watch(selectedStatuses, (val) => {
|
|||
|
||||
watch(() => props.id, () => {
|
||||
taskSearch.value = ''
|
||||
environments.value = []
|
||||
showScanBanner.value = false
|
||||
scanTaskId.value = null
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
@ -287,6 +367,7 @@ onMounted(async () => {
|
|||
loadMode()
|
||||
loadAutocommit()
|
||||
await loadPhases()
|
||||
await loadEnvironments()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
@ -384,16 +465,25 @@ async function addTask() {
|
|||
}
|
||||
}
|
||||
|
||||
const runningTaskId = ref<string | null>(null)
|
||||
|
||||
async function runTask(taskId: string, event: Event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!confirm(`Run pipeline for ${taskId}?`)) return
|
||||
runningTaskId.value = taskId
|
||||
try {
|
||||
await api.runTask(taskId)
|
||||
await load()
|
||||
if (activeTab.value === 'kanban') checkAndPollKanban()
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
if (e instanceof ApiError && e.code === 'task_already_running') {
|
||||
error.value = 'Pipeline уже запущен'
|
||||
} else {
|
||||
error.value = e.message
|
||||
}
|
||||
} finally {
|
||||
runningTaskId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -518,6 +608,8 @@ async function addDecision() {
|
|||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<router-link to="/" class="text-gray-600 hover:text-gray-400 text-sm no-underline">← back</router-link>
|
||||
<span class="text-gray-700">|</span>
|
||||
<router-link :to="`/chat/${project.id}`" class="text-indigo-500 hover:text-indigo-400 text-sm no-underline">Чат</router-link>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h1 class="text-xl font-bold text-gray-100">{{ project.id }}</h1>
|
||||
|
|
@ -545,18 +637,19 @@ async function addDecision() {
|
|||
|
||||
<!-- Tabs -->
|
||||
<div class="flex gap-1 mb-4 border-b border-gray-800">
|
||||
<button v-for="tab in (['tasks', 'phases', 'decisions', 'modules', 'kanban'] as const)" :key="tab"
|
||||
<button v-for="tab in (['tasks', 'phases', 'decisions', 'modules', 'kanban', 'environments'] as const)" :key="tab"
|
||||
@click="activeTab = tab"
|
||||
class="px-4 py-2 text-sm border-b-2 transition-colors"
|
||||
:class="activeTab === tab
|
||||
? 'text-gray-200 border-blue-500'
|
||||
: 'text-gray-500 border-transparent hover:text-gray-300'">
|
||||
{{ tab === 'kanban' ? 'Kanban' : tab.charAt(0).toUpperCase() + tab.slice(1) }}
|
||||
{{ tab === 'kanban' ? 'Kanban' : tab === 'environments' ? 'Среды' : tab.charAt(0).toUpperCase() + tab.slice(1) }}
|
||||
<span class="text-xs text-gray-600 ml-1">
|
||||
{{ tab === 'tasks' ? project.tasks.length
|
||||
: tab === 'phases' ? phases.length
|
||||
: tab === 'decisions' ? project.decisions.length
|
||||
: tab === 'modules' ? project.modules.length
|
||||
: tab === 'environments' ? environments.length
|
||||
: project.tasks.length }}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -697,8 +790,12 @@ async function addDecision() {
|
|||
</select>
|
||||
<button v-if="t.status === 'pending'"
|
||||
@click="runTask(t.id, $event)"
|
||||
class="px-2 py-0.5 bg-blue-900/40 text-blue-400 border border-blue-800 rounded hover:bg-blue-900 text-[10px]"
|
||||
title="Run pipeline">▶</button>
|
||||
:disabled="runningTaskId === t.id"
|
||||
class="px-2 py-0.5 bg-blue-900/40 text-blue-400 border border-blue-800 rounded hover:bg-blue-900 text-[10px] disabled:opacity-50"
|
||||
title="Run pipeline">
|
||||
<span v-if="runningTaskId === t.id" class="inline-block w-2 h-2 border border-blue-400 border-t-transparent rounded-full animate-spin"></span>
|
||||
<span v-else>▶</span>
|
||||
</button>
|
||||
<span v-if="t.status === 'in_progress'"
|
||||
class="inline-block w-2 h-2 bg-blue-500 rounded-full animate-pulse" title="Running"></span>
|
||||
</div>
|
||||
|
|
@ -939,6 +1036,61 @@ async function addDecision() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environments Tab -->
|
||||
<div v-if="activeTab === 'environments'">
|
||||
<!-- Scan started banner -->
|
||||
<div v-if="showScanBanner" class="mb-4 px-4 py-3 border border-blue-700 bg-blue-950/30 rounded flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-blue-300">🔍 Запускаем сканирование среды...</p>
|
||||
<p class="text-xs text-blue-200/70 mt-1">Создана задача сисадмина:
|
||||
<router-link v-if="scanTaskId" :to="`/task/${scanTaskId}`" class="text-blue-400 hover:text-blue-300 no-underline">{{ scanTaskId }}</router-link>
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mt-1">Агент опишет среду, установленное ПО и настроенный git. При нехватке данных — эскалация к вам.</p>
|
||||
</div>
|
||||
<button @click="showScanBanner = false" class="text-gray-600 hover:text-gray-400 bg-transparent border-none cursor-pointer text-xs shrink-0">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-xs text-gray-500">Серверные окружения проекта</span>
|
||||
<button @click="openEnvModal()"
|
||||
class="px-3 py-1 text-xs bg-gray-800 text-gray-300 border border-gray-700 rounded hover:bg-gray-700">
|
||||
+ Среда
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="envsLoading" class="text-gray-500 text-sm">Загрузка...</p>
|
||||
<p v-else-if="envsError" class="text-red-400 text-sm">{{ envsError }}</p>
|
||||
<div v-else-if="environments.length === 0" class="text-gray-600 text-sm">Нет сред. Добавьте сервер для развёртывания.</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="env in environments" :key="env.id"
|
||||
class="px-4 py-3 border border-gray-800 rounded hover:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-gray-200">{{ env.name }}</span>
|
||||
<span class="px-1.5 py-0.5 text-[10px] rounded border"
|
||||
:class="env.is_installed ? 'bg-green-900/30 text-green-400 border-green-800' : 'bg-gray-800 text-gray-500 border-gray-700'">
|
||||
{{ env.is_installed ? '✓ установлен' : 'не установлен' }}
|
||||
</span>
|
||||
<span class="px-1.5 py-0.5 text-[10px] bg-gray-800 text-gray-500 border border-gray-700 rounded">{{ env.auth_type }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="openEnvModal(env)" title="Редактировать"
|
||||
class="px-2 py-0.5 text-xs bg-gray-800 text-gray-400 border border-gray-700 rounded hover:bg-gray-700 hover:text-gray-200">
|
||||
✎
|
||||
</button>
|
||||
<button @click="deleteEnv(env.id)" title="Удалить"
|
||||
class="px-2 py-0.5 text-xs bg-gray-800 text-red-500 border border-gray-700 rounded hover:bg-red-950/30 hover:border-red-800">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-500 flex gap-3 flex-wrap">
|
||||
<span><span class="text-gray-600">host:</span> <span class="text-orange-400">{{ env.username }}@{{ env.host }}:{{ env.port }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Task Modal -->
|
||||
<Modal v-if="showAddTask" title="Add Task" @close="showAddTask = false">
|
||||
<form @submit.prevent="addTask" class="space-y-3">
|
||||
|
|
@ -1000,6 +1152,72 @@ async function addDecision() {
|
|||
</form>
|
||||
</Modal>
|
||||
|
||||
<!-- Environment Modal -->
|
||||
<Modal v-if="showEnvModal" :title="editingEnv ? 'Редактировать среду' : 'Добавить среду'" @close="showEnvModal = false">
|
||||
<form @submit.prevent="submitEnv" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Название</label>
|
||||
<select v-model="envForm.name"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300">
|
||||
<option value="prod">prod</option>
|
||||
<option value="dev">dev</option>
|
||||
<option value="staging">staging</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs text-gray-500 mb-1">Host (IP или домен)</label>
|
||||
<input v-model="envForm.host" placeholder="10.0.0.1" required
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
|
||||
</div>
|
||||
<div class="w-24">
|
||||
<label class="block text-xs text-gray-500 mb-1">Port</label>
|
||||
<input v-model.number="envForm.port" type="number" min="1" max="65535"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Login</label>
|
||||
<input v-model="envForm.username" placeholder="root" required
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-2">Тип авторизации</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-1.5 text-sm text-gray-300 cursor-pointer">
|
||||
<input type="radio" v-model="envForm.auth_type" value="password" class="accent-blue-500" />
|
||||
Пароль
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-sm text-gray-300 cursor-pointer">
|
||||
<input type="radio" v-model="envForm.auth_type" value="key" class="accent-blue-500" />
|
||||
SSH ключ
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">{{ envForm.auth_type === 'key' ? 'SSH ключ (private key)' : 'Пароль' }}</label>
|
||||
<textarea v-if="envForm.auth_type === 'key'" v-model="envForm.auth_value" rows="4"
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ..."
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-xs text-gray-200 placeholder-gray-600 resize-y font-mono"></textarea>
|
||||
<input v-else v-model="envForm.auth_value" type="password"
|
||||
:placeholder="editingEnv ? 'Оставьте пустым, чтобы не менять' : 'Пароль'"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
|
||||
</div>
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="checkbox" v-model="envForm.is_installed" class="accent-blue-500" />
|
||||
<span class="text-sm text-gray-300">☑ Проект уже установлен на сервере</span>
|
||||
</label>
|
||||
<div v-if="envForm.is_installed" class="px-3 py-2 border border-blue-800 bg-blue-950/20 rounded text-xs text-blue-300">
|
||||
После сохранения будет запущен агент-сисадмин для сканирования среды.
|
||||
</div>
|
||||
<p v-if="envFormError" class="text-red-400 text-xs">{{ envFormError }}</p>
|
||||
<button type="submit" :disabled="envSaving"
|
||||
class="w-full py-2 bg-blue-900/50 text-blue-400 border border-blue-800 rounded text-sm hover:bg-blue-900 disabled:opacity-50">
|
||||
{{ envSaving ? 'Сохраняем...' : editingEnv ? 'Сохранить' : 'Добавить' }}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<!-- Audit Modal -->
|
||||
<Modal v-if="showAuditModal && auditResult" title="Backlog Audit Results" @close="showAuditModal = false">
|
||||
<div v-if="!auditResult.success" class="text-red-400 text-sm">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue