kin: KIN-089 При попытке добавить креды прод сервера для проекта corelock вылетает 500 Internal Server Error

This commit is contained in:
Gros Frumos 2026-03-16 20:39:17 +02:00
parent e80e50ba0c
commit 4a65d90218
13 changed files with 1215 additions and 4 deletions

View file

@ -53,6 +53,12 @@ async function del<T>(path: string): Promise<T> {
return res.json()
}
async function postForm<T>(path: string, body: FormData): Promise<T> {
const res = await fetch(`${BASE}${path}`, { method: 'POST', body })
if (!res.ok) await throwApiError(res)
return res.json()
}
export interface Project {
id: string
name: string
@ -270,6 +276,15 @@ export interface ChatSendResult {
task?: Task | null
}
export interface Attachment {
id: number
task_id: string
filename: string
mime_type: string
size: number
created_at: string
}
export const api = {
projects: () => get<Project[]>('/projects'),
project: (id: string) => get<ProjectDetail>(`/projects/${id}`),
@ -339,4 +354,14 @@ export const api = {
get<ChatMessage[]>(`/projects/${projectId}/chat?limit=${limit}`),
sendChatMessage: (projectId: string, content: string) =>
post<ChatSendResult>(`/projects/${projectId}/chat`, { content }),
uploadAttachment: (taskId: string, file: File) => {
const fd = new FormData()
fd.append('file', file)
return postForm<Attachment>(`/tasks/${taskId}/attachments`, fd)
},
getAttachments: (taskId: string) =>
get<Attachment[]>(`/tasks/${taskId}/attachments`),
deleteAttachment: (taskId: string, id: number) =>
del<void>(`/tasks/${taskId}/attachments/${id}`),
attachmentUrl: (id: number) => `${BASE}/attachments/${id}/file`,
}

View file

@ -0,0 +1,55 @@
<script setup lang="ts">
import { ref } from 'vue'
import { api, type Attachment } from '../api'
const props = defineProps<{ attachments: Attachment[]; taskId: string }>()
const emit = defineEmits<{ deleted: [] }>()
const deletingId = ref<number | null>(null)
async function remove(id: number) {
deletingId.value = id
try {
await api.deleteAttachment(props.taskId, id)
emit('deleted')
} catch {
// silently ignore parent will reload
} finally {
deletingId.value = null
}
}
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes}B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
return `${(bytes / 1024 / 1024).toFixed(1)}MB`
}
</script>
<template>
<div v-if="attachments.length" class="flex flex-wrap gap-3 mb-3">
<div
v-for="att in attachments"
:key="att.id"
class="relative group border border-gray-700 rounded-lg overflow-hidden bg-gray-900 w-28"
>
<a :href="api.attachmentUrl(att.id)" target="_blank" rel="noopener">
<img
:src="api.attachmentUrl(att.id)"
:alt="att.filename"
class="w-28 h-20 object-cover block"
/>
</a>
<div class="px-1.5 py-1">
<p class="text-[10px] text-gray-400 truncate" :title="att.filename">{{ att.filename }}</p>
<p class="text-[10px] text-gray-600">{{ formatSize(att.size) }}</p>
</div>
<button
@click="remove(att.id)"
:disabled="deletingId === att.id"
class="absolute top-1 right-1 w-5 h-5 rounded-full bg-red-900/80 text-red-400 text-xs leading-none opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-50 flex items-center justify-center"
title="Удалить"
></button>
</div>
</div>
</template>

View file

@ -0,0 +1,62 @@
<script setup lang="ts">
import { ref } from 'vue'
import { api } from '../api'
const props = defineProps<{ taskId: string }>()
const emit = defineEmits<{ uploaded: [] }>()
const dragging = ref(false)
const uploading = ref(false)
const error = ref('')
const fileInput = ref<HTMLInputElement | null>(null)
async function upload(file: File) {
if (!file.type.startsWith('image/')) {
error.value = 'Поддерживаются только изображения'
return
}
uploading.value = true
error.value = ''
try {
await api.uploadAttachment(props.taskId, file)
emit('uploaded')
} catch (e: any) {
error.value = e.message
} finally {
uploading.value = false
}
}
function onFileChange(event: Event) {
const input = event.target as HTMLInputElement
if (input.files?.[0]) upload(input.files[0])
input.value = ''
}
function onDrop(event: DragEvent) {
dragging.value = false
const file = event.dataTransfer?.files[0]
if (file) upload(file)
}
</script>
<template>
<div
class="border-2 border-dashed rounded-lg p-3 text-center transition-colors cursor-pointer select-none"
:class="dragging ? 'border-blue-500 bg-blue-950/20' : 'border-gray-700 hover:border-gray-500'"
@dragover.prevent="dragging = true"
@dragleave="dragging = false"
@drop.prevent="onDrop"
@click="fileInput?.click()"
>
<input ref="fileInput" type="file" accept="image/*" class="hidden" @change="onFileChange" />
<div v-if="uploading" class="flex items-center justify-center gap-2 text-xs text-blue-400">
<span class="inline-block w-3 h-3 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></span>
Загрузка...
</div>
<div v-else class="text-xs text-gray-500">
Перетащите изображение или <span class="text-blue-400">нажмите для выбора</span>
</div>
<p v-if="error" class="text-red-400 text-xs mt-1">{{ error }}</p>
</div>
</template>

View file

@ -1,9 +1,11 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { api, ApiError, type TaskFull, type PipelineStep, type PendingAction, type DeployResult } from '../api'
import { api, ApiError, type TaskFull, type PipelineStep, type PendingAction, type DeployResult, type Attachment } from '../api'
import Badge from '../components/Badge.vue'
import Modal from '../components/Modal.vue'
import AttachmentUploader from '../components/AttachmentUploader.vue'
import AttachmentList from '../components/AttachmentList.vue'
const props = defineProps<{ id: string }>()
const route = useRoute()
@ -92,7 +94,7 @@ function stopPolling() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
}
onMounted(load)
onMounted(() => { load(); loadAttachments() })
onUnmounted(stopPolling)
function statusColor(s: string) {
@ -291,6 +293,15 @@ async function runDeploy() {
}
}
// Attachments
const attachments = ref<Attachment[]>([])
async function loadAttachments() {
try {
attachments.value = await api.getAttachments(props.id)
} catch {}
}
// Edit modal (pending tasks only)
const showEdit = ref(false)
const editForm = ref({ title: '', briefText: '', priority: 5, acceptanceCriteria: '' })
@ -474,6 +485,13 @@ async function saveEdit() {
</div>
</div>
<!-- Attachments -->
<div class="mb-6">
<h2 class="text-sm font-semibold text-gray-300 mb-2">Вложения</h2>
<AttachmentList :attachments="attachments" :task-id="props.id" @deleted="loadAttachments" />
<AttachmentUploader :task-id="props.id" @uploaded="loadAttachments" />
</div>
<!-- Actions Bar -->
<div class="sticky bottom-0 bg-gray-950 border-t border-gray-800 py-3 flex gap-3 -mx-6 px-6 mt-8">
<div v-if="autoMode && (isRunning || task.status === 'review')"