kin: KIN-108-frontend_dev

This commit is contained in:
Gros Frumos 2026-03-18 07:57:15 +02:00
parent 8b409fd7db
commit 353416ead1
16 changed files with 799 additions and 212 deletions

View file

@ -1,9 +1,12 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { api, type Project, type CostEntry } from '../api'
import Badge from '../components/Badge.vue'
import Modal from '../components/Modal.vue'
const { t } = useI18n()
const projects = ref<Project[]>([])
const costs = ref<CostEntry[]>([])
const loading = ref(true)
@ -25,12 +28,12 @@ const bsResult = ref('')
// New Project with Research modal
const RESEARCH_ROLES = [
{ key: 'business_analyst', label: 'Business Analyst', hint: 'бизнес-модель, аудитория, монетизация' },
{ key: 'market_researcher', label: 'Market Researcher', hint: 'конкуренты, ниша, сильные/слабые стороны' },
{ key: 'legal_researcher', label: 'Legal Researcher', hint: 'юрисдикция, лицензии, KYC/AML, GDPR' },
{ key: 'tech_researcher', label: 'Tech Researcher', hint: 'API, ограничения, стоимость, альтернативы' },
{ key: 'ux_designer', label: 'UX Designer', hint: 'анализ UX конкурентов, user journey, wireframes' },
{ key: 'marketer', label: 'Marketer', hint: 'стратегия продвижения, SEO, conversion-паттерны' },
{ key: 'business_analyst' },
{ key: 'market_researcher' },
{ key: 'legal_researcher' },
{ key: 'tech_researcher' },
{ key: 'ux_designer' },
{ key: 'marketer' },
]
const showNewProject = ref(false)
const npForm = ref({
@ -55,7 +58,6 @@ let dashPollTimer: ReturnType<typeof setInterval> | null = null
onMounted(async () => {
await load()
// Poll if there are running tasks
checkAndPoll()
})
@ -87,11 +89,11 @@ function statusColor(s: string) {
async function addProject() {
formError.value = ''
if (form.value.project_type === 'operations' && !form.value.ssh_host) {
formError.value = 'SSH host is required for operations projects'
formError.value = t('dashboard.ssh_host_required')
return
}
if (form.value.project_type !== 'operations' && !form.value.path) {
formError.value = 'Path is required'
formError.value = t('dashboard.path_required')
return
}
try {
@ -157,7 +159,7 @@ async function deleteProject(id: string) {
async function createNewProject() {
npError.value = ''
if (!npRoles.value.length) {
npError.value = 'Выберите хотя бы одну роль'
npError.value = t('dashboard.role_error')
return
}
npSaving.value = true
@ -189,26 +191,26 @@ async function createNewProject() {
<div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-xl font-bold text-gray-100">Dashboard</h1>
<p class="text-sm text-gray-500" v-if="totalCost > 0">Cost this week: ${{ totalCost.toFixed(2) }}</p>
<h1 class="text-xl font-bold text-gray-100">{{ t('dashboard.title') }}</h1>
<p class="text-sm text-gray-500" v-if="totalCost > 0">{{ t('dashboard.cost_this_week') }}: ${{ totalCost.toFixed(2) }}</p>
</div>
<div class="flex gap-2">
<button @click="showBootstrap = true"
class="px-3 py-1.5 text-xs bg-purple-900/50 text-purple-400 border border-purple-800 rounded hover:bg-purple-900">
Bootstrap
{{ t('dashboard.bootstrap') }}
</button>
<button @click="showNewProject = true"
class="px-3 py-1.5 text-xs bg-green-900/50 text-green-400 border border-green-800 rounded hover:bg-green-900">
+ New Project
{{ t('dashboard.new_project') }}
</button>
<button @click="showAdd = true"
class="px-3 py-1.5 text-xs bg-gray-800 text-gray-300 border border-gray-700 rounded hover:bg-gray-700">
+ Blank
{{ t('dashboard.blank') }}
</button>
</div>
</div>
<p v-if="loading" class="text-gray-500 text-sm">Loading...</p>
<p v-if="loading" class="text-gray-500 text-sm">{{ t('dashboard.loading') }}</p>
<p v-else-if="error" class="text-red-400 text-sm">{{ error }}</p>
<div v-else class="grid gap-3">
@ -216,17 +218,17 @@ async function createNewProject() {
<!-- Inline delete confirmation -->
<div v-if="confirmDeleteId === p.id"
class="border border-red-800 rounded-lg p-4 bg-red-950/20">
<p class="text-sm text-gray-200 mb-3">Удалить проект «{{ p.name }}»? Это действие необратимо.</p>
<p class="text-sm text-gray-200 mb-3">{{ t('dashboard.delete_confirm', { name: p.name }) }}</p>
<div class="flex gap-2">
<button @click="deleteProject(p.id)"
title="Подтвердить удаление"
:title="t('dashboard.delete_project_title')"
class="px-3 py-1.5 text-xs bg-red-900/50 text-red-400 border border-red-800 rounded hover:bg-red-900">
Да, удалить
{{ t('common.yes_delete') }}
</button>
<button @click="confirmDeleteId = null"
title="Отмена удаления"
:title="t('dashboard.cancel_delete_title')"
class="px-3 py-1.5 text-xs bg-gray-800 text-gray-400 border border-gray-700 rounded hover:bg-gray-700">
Отмена
{{ t('common.cancel') }}
</button>
</div>
<p v-if="deleteError" class="text-red-400 text-xs mt-2">{{ deleteError }}</p>
@ -249,7 +251,7 @@ async function createNewProject() {
<span v-if="costMap[p.id]">${{ costMap[p.id]?.toFixed(2) }}/wk</span>
<span>pri {{ p.priority }}</span>
<button @click.prevent.stop="confirmDeleteId = p.id"
title="Удалить проект"
:title="t('common.delete')"
class="text-gray-600 hover:text-red-400 transition-colors">
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
@ -275,82 +277,82 @@ async function createNewProject() {
</div>
<!-- Add Project Modal -->
<Modal v-if="showAdd" title="Add Project" @close="showAdd = false">
<Modal v-if="showAdd" :title="t('dashboard.add_project_title')" @close="showAdd = false">
<form @submit.prevent="addProject" class="space-y-3">
<input v-model="form.id" placeholder="ID (e.g. vdol)" required
<input v-model="form.id" :placeholder="t('dashboard.id_placeholder')" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<input v-model="form.name" placeholder="Name" required
<input v-model="form.name" :placeholder="t('dashboard.name_placeholder')" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<!-- Project type selector -->
<div>
<p class="text-xs text-gray-500 mb-1.5">Тип проекта:</p>
<p class="text-xs text-gray-500 mb-1.5">{{ t('dashboard.project_type_label') }}</p>
<div class="flex gap-2">
<button v-for="t in ['development', 'operations', 'research']" :key="t"
<button v-for="t_type in ['development', 'operations', 'research']" :key="t_type"
type="button"
@click="form.project_type = t"
@click="form.project_type = t_type"
class="flex-1 py-1.5 text-xs border rounded transition-colors"
:class="form.project_type === t
? t === 'development' ? 'bg-blue-900/40 text-blue-300 border-blue-700'
: t === 'operations' ? 'bg-orange-900/40 text-orange-300 border-orange-700'
:class="form.project_type === t_type
? t_type === 'development' ? 'bg-blue-900/40 text-blue-300 border-blue-700'
: t_type === 'operations' ? 'bg-orange-900/40 text-orange-300 border-orange-700'
: 'bg-green-900/40 text-green-300 border-green-700'
: 'bg-gray-900 text-gray-500 border-gray-800 hover:text-gray-300 hover:border-gray-600'"
>{{ t }}</button>
>{{ t_type }}</button>
</div>
</div>
<!-- Path (development / research) -->
<input v-if="form.project_type !== 'operations'"
v-model="form.path" placeholder="Path (e.g. ~/projects/myproj)"
v-model="form.path" :placeholder="t('dashboard.path_placeholder')"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<!-- SSH fields (operations) -->
<template v-if="form.project_type === 'operations'">
<input v-model="form.ssh_host" placeholder="SSH host (e.g. 192.168.1.1)" required
<input v-model="form.ssh_host" :placeholder="t('dashboard.ssh_host_placeholder')" required
class="w-full bg-gray-800 border border-orange-800/60 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<div class="grid grid-cols-2 gap-2">
<input v-model="form.ssh_user" placeholder="SSH user (e.g. root)"
<input v-model="form.ssh_user" :placeholder="t('dashboard.ssh_user_placeholder')"
class="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<input v-model="form.ssh_key_path" placeholder="Key path (e.g. ~/.ssh/id_rsa)"
<input v-model="form.ssh_key_path" :placeholder="t('dashboard.ssh_key_placeholder')"
class="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
</div>
<div>
<input v-model="form.ssh_proxy_jump" placeholder="ProxyJump (optional, e.g. jumpt)"
<input v-model="form.ssh_proxy_jump" :placeholder="t('dashboard.proxy_jump_placeholder')"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<p class="mt-1 flex items-center gap-1 text-xs text-gray-500">
<svg class="w-3 h-3 flex-shrink-0 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
Алиас из ~/.ssh/config на сервере Kin
{{ t('dashboard.ssh_alias_hint') }}
</p>
</div>
</template>
<input v-model="form.tech_stack" placeholder="Tech stack (comma-separated)"
<input v-model="form.tech_stack" :placeholder="t('dashboard.tech_stack_placeholder')"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<input v-model.number="form.priority" type="number" min="1" max="10" placeholder="Priority (1-10)"
<input v-model.number="form.priority" type="number" min="1" max="10" :placeholder="t('dashboard.priority_placeholder')"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<p v-if="formError" class="text-red-400 text-xs">{{ formError }}</p>
<button type="submit"
class="w-full py-2 bg-blue-900/50 text-blue-400 border border-blue-800 rounded text-sm hover:bg-blue-900">
Create
{{ t('dashboard.create_btn') }}
</button>
</form>
</Modal>
<!-- New Project with Research Modal -->
<Modal v-if="showNewProject" title="New Project — Start Research" @close="showNewProject = false">
<Modal v-if="showNewProject" :title="t('dashboard.new_project_title')" @close="showNewProject = false">
<form @submit.prevent="createNewProject" class="space-y-3">
<div class="grid grid-cols-2 gap-2">
<input v-model="npForm.id" placeholder="ID (e.g. myapp)" required
<input v-model="npForm.id" :placeholder="t('dashboard.id_placeholder')" required
class="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<input v-model="npForm.name" placeholder="Name" required
<input v-model="npForm.name" :placeholder="t('dashboard.name_placeholder')" required
class="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
</div>
<input v-model="npForm.path" placeholder="Path (e.g. ~/projects/myapp)"
<input v-model="npForm.path" :placeholder="t('dashboard.path_placeholder')"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<textarea v-model="npForm.description" placeholder="Описание проекта (свободный текст для агентов)" required rows="4"
<textarea v-model="npForm.description" :placeholder="t('dashboard.project_description_placeholder')" required rows="4"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 resize-none"></textarea>
<input v-model="npForm.tech_stack" placeholder="Tech stack (comma-separated, optional)"
<input v-model="npForm.tech_stack" :placeholder="t('dashboard.tech_stack_placeholder')"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<div>
<p class="text-xs text-gray-500 mb-2">Этапы research (Architect добавляется автоматически последним):</p>
<p class="text-xs text-gray-500 mb-2">{{ t('dashboard.research_stages') }}</p>
<div class="space-y-1.5">
<label v-for="r in RESEARCH_ROLES" :key="r.key"
class="flex items-start gap-2 cursor-pointer group">
@ -359,15 +361,15 @@ async function createNewProject() {
@change="toggleNpRole(r.key)"
class="mt-0.5 accent-green-500 cursor-pointer" />
<div>
<span class="text-sm text-gray-300 group-hover:text-gray-100">{{ r.label }}</span>
<span class="text-xs text-gray-600 ml-1"> {{ r.hint }}</span>
<span class="text-sm text-gray-300 group-hover:text-gray-100">{{ t(`dashboard.roles.${r.key}.label`) }}</span>
<span class="text-xs text-gray-600 ml-1"> {{ t(`dashboard.roles.${r.key}.hint`) }}</span>
</div>
</label>
<label class="flex items-start gap-2 opacity-50">
<input type="checkbox" checked disabled class="mt-0.5" />
<div>
<span class="text-sm text-gray-400">Architect</span>
<span class="text-xs text-gray-600 ml-1"> blueprint на основе одобренных исследований</span>
<span class="text-sm text-gray-400">{{ t('dashboard.roles.architect.label') }}</span>
<span class="text-xs text-gray-600 ml-1"> {{ t('dashboard.architect_hint') }}</span>
</div>
</label>
</div>
@ -375,25 +377,25 @@ async function createNewProject() {
<p v-if="npError" class="text-red-400 text-xs">{{ npError }}</p>
<button type="submit" :disabled="npSaving"
class="w-full py-2 bg-green-900/50 text-green-400 border border-green-800 rounded text-sm hover:bg-green-900 disabled:opacity-50">
{{ npSaving ? 'Starting...' : 'Start Research' }}
{{ npSaving ? t('dashboard.starting') : t('dashboard.start_research') }}
</button>
</form>
</Modal>
<!-- Bootstrap Modal -->
<Modal v-if="showBootstrap" title="Bootstrap Project" @close="showBootstrap = false">
<Modal v-if="showBootstrap" :title="t('dashboard.bootstrap_title')" @close="showBootstrap = false">
<form @submit.prevent="runBootstrap" class="space-y-3">
<input v-model="bsForm.path" placeholder="Project path (e.g. ~/projects/vdolipoperek)" required
<input v-model="bsForm.path" :placeholder="t('dashboard.bootstrap_path_placeholder')" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<input v-model="bsForm.id" placeholder="ID (e.g. vdol)" required
<input v-model="bsForm.id" :placeholder="t('dashboard.id_placeholder')" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<input v-model="bsForm.name" placeholder="Name" required
<input v-model="bsForm.name" :placeholder="t('dashboard.name_placeholder')" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
<p v-if="bsError" class="text-red-400 text-xs">{{ bsError }}</p>
<p v-if="bsResult" class="text-green-400 text-xs">{{ bsResult }}</p>
<button type="submit"
class="w-full py-2 bg-purple-900/50 text-purple-400 border border-purple-800 rounded text-sm hover:bg-purple-900">
Bootstrap
{{ t('dashboard.bootstrap_btn') }}
</button>
</form>
</Modal>