Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<script setup lang="ts">
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
2026-03-15 20:02:01 +02:00
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-03-18 07:57:15 +02:00
|
|
|
|
import { useI18n } from 'vue-i18n'
|
2026-03-18 14:30:36 +02:00
|
|
|
|
import { api, ApiError, type ProjectDetail, type AuditResult, type Phase, type Task, type ProjectEnvironment, type DeployResult, type ProjectLink, type ObsidianSyncResult } from '../api'
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
import Badge from '../components/Badge.vue'
|
|
|
|
|
|
import Modal from '../components/Modal.vue'
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{ id: string }>()
|
2026-03-15 20:02:01 +02:00
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
2026-03-18 07:57:15 +02:00
|
|
|
|
const { t } = useI18n()
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
|
|
|
|
|
|
const project = ref<ProjectDetail | null>(null)
|
|
|
|
|
|
const loading = ref(true)
|
|
|
|
|
|
const error = ref('')
|
2026-03-18 14:30:36 +02:00
|
|
|
|
const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments' | 'links' | 'settings'>('tasks')
|
2026-03-16 09:13:34 +02:00
|
|
|
|
|
|
|
|
|
|
// Phases
|
|
|
|
|
|
const phases = ref<Phase[]>([])
|
|
|
|
|
|
const phasesLoading = ref(false)
|
|
|
|
|
|
const phaseError = ref('')
|
2026-03-16 15:48:09 +02:00
|
|
|
|
const claudeLoginError = ref(false)
|
2026-03-16 09:13:34 +02:00
|
|
|
|
const showReviseModal = ref(false)
|
|
|
|
|
|
const revisePhaseId = ref<number | null>(null)
|
|
|
|
|
|
const reviseComment = ref('')
|
|
|
|
|
|
const reviseError = ref('')
|
|
|
|
|
|
const reviseSaving = ref(false)
|
|
|
|
|
|
const showRejectModal = ref(false)
|
|
|
|
|
|
const rejectPhaseId = ref<number | null>(null)
|
|
|
|
|
|
const rejectReason = ref('')
|
|
|
|
|
|
const rejectError = ref('')
|
|
|
|
|
|
const rejectSaving = ref(false)
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
const startPhaseSaving = ref(false)
|
|
|
|
|
|
const approvePhaseSaving = ref(false)
|
|
|
|
|
|
let phasePollTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
|
|
2026-03-16 22:35:31 +02:00
|
|
|
|
// Task Revise
|
|
|
|
|
|
const showTaskReviseModal = ref(false)
|
|
|
|
|
|
const taskReviseTaskId = ref<string | null>(null)
|
|
|
|
|
|
const taskReviseComment = ref('')
|
|
|
|
|
|
const taskReviseError = ref('')
|
|
|
|
|
|
const taskReviseSaving = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
function openTaskRevise(taskId: string) {
|
|
|
|
|
|
taskReviseTaskId.value = taskId
|
|
|
|
|
|
taskReviseComment.value = ''
|
|
|
|
|
|
taskReviseError.value = ''
|
|
|
|
|
|
showTaskReviseModal.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitTaskRevise() {
|
2026-03-18 07:57:15 +02:00
|
|
|
|
if (!taskReviseComment.value.trim()) { taskReviseError.value = t('projectView.comment_required'); return }
|
2026-03-16 22:35:31 +02:00
|
|
|
|
taskReviseSaving.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.reviseTask(taskReviseTaskId.value!, taskReviseComment.value)
|
|
|
|
|
|
showTaskReviseModal.value = false
|
|
|
|
|
|
await load()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
taskReviseError.value = e.message
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
taskReviseSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
function checkAndPollPhases() {
|
|
|
|
|
|
const hasRunning = phases.value.some(ph => ph.task?.status === 'in_progress')
|
|
|
|
|
|
if (hasRunning && !phasePollTimer) {
|
|
|
|
|
|
phasePollTimer = setInterval(async () => {
|
|
|
|
|
|
phases.value = await api.getPhases(props.id).catch(() => phases.value)
|
|
|
|
|
|
if (!phases.value.some(ph => ph.task?.status === 'in_progress')) {
|
|
|
|
|
|
clearInterval(phasePollTimer!)
|
|
|
|
|
|
phasePollTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000)
|
|
|
|
|
|
} else if (!hasRunning && phasePollTimer) {
|
|
|
|
|
|
clearInterval(phasePollTimer)
|
|
|
|
|
|
phasePollTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-16 09:13:34 +02:00
|
|
|
|
|
|
|
|
|
|
async function loadPhases() {
|
|
|
|
|
|
phasesLoading.value = true
|
|
|
|
|
|
phaseError.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
phases.value = await api.getPhases(props.id)
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
checkAndPollPhases()
|
2026-03-16 09:13:34 +02:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
phaseError.value = e.message
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
phasesLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function approvePhase(phaseId: number) {
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
approvePhaseSaving.value = true
|
2026-03-16 09:13:34 +02:00
|
|
|
|
try {
|
|
|
|
|
|
await api.approvePhase(phaseId)
|
|
|
|
|
|
await loadPhases()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
phaseError.value = e.message
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
} finally {
|
|
|
|
|
|
approvePhaseSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function startPhase() {
|
|
|
|
|
|
startPhaseSaving.value = true
|
|
|
|
|
|
phaseError.value = ''
|
2026-03-16 15:48:09 +02:00
|
|
|
|
claudeLoginError.value = false
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
try {
|
|
|
|
|
|
await api.startPhase(props.id)
|
|
|
|
|
|
await loadPhases()
|
|
|
|
|
|
} catch (e: any) {
|
2026-03-16 15:48:09 +02:00
|
|
|
|
if (e instanceof ApiError && e.code === 'claude_auth_required') {
|
|
|
|
|
|
claudeLoginError.value = true
|
|
|
|
|
|
} else {
|
|
|
|
|
|
phaseError.value = e.message
|
|
|
|
|
|
}
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
} finally {
|
|
|
|
|
|
startPhaseSaving.value = false
|
2026-03-16 09:13:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openRevise(phaseId: number) {
|
|
|
|
|
|
revisePhaseId.value = phaseId
|
|
|
|
|
|
reviseComment.value = ''
|
|
|
|
|
|
reviseError.value = ''
|
|
|
|
|
|
showReviseModal.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitRevise() {
|
|
|
|
|
|
if (!reviseComment.value.trim()) { reviseError.value = 'Comment required'; return }
|
|
|
|
|
|
reviseSaving.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.revisePhase(revisePhaseId.value!, reviseComment.value)
|
|
|
|
|
|
showReviseModal.value = false
|
|
|
|
|
|
await loadPhases()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
reviseError.value = e.message
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
reviseSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openReject(phaseId: number) {
|
|
|
|
|
|
rejectPhaseId.value = phaseId
|
|
|
|
|
|
rejectReason.value = ''
|
|
|
|
|
|
rejectError.value = ''
|
|
|
|
|
|
showRejectModal.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitReject() {
|
|
|
|
|
|
if (!rejectReason.value.trim()) { rejectError.value = 'Reason required'; return }
|
|
|
|
|
|
rejectSaving.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.rejectPhase(rejectPhaseId.value!, rejectReason.value)
|
|
|
|
|
|
showRejectModal.value = false
|
|
|
|
|
|
await loadPhases()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
rejectError.value = e.message
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
rejectSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function phaseStatusColor(s: string) {
|
|
|
|
|
|
const m: Record<string, string> = {
|
|
|
|
|
|
pending: 'gray', active: 'blue', approved: 'green',
|
|
|
|
|
|
rejected: 'red', revising: 'yellow',
|
|
|
|
|
|
}
|
|
|
|
|
|
return m[s] || 'gray'
|
|
|
|
|
|
}
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
|
|
|
|
|
|
// Filters
|
2026-03-16 22:35:31 +02:00
|
|
|
|
const ALL_TASK_STATUSES = ['pending', 'in_progress', 'review', 'blocked', 'decomposed', 'done', 'revising', 'cancelled']
|
2026-03-15 23:22:49 +02:00
|
|
|
|
|
|
|
|
|
|
function initStatusFilter(): string[] {
|
|
|
|
|
|
const q = route.query.status as string
|
|
|
|
|
|
if (q) return q.split(',').filter((s: string) => s)
|
|
|
|
|
|
const stored = localStorage.getItem(`kin-task-statuses-${props.id}`)
|
|
|
|
|
|
if (stored) { try { return JSON.parse(stored) } catch {} }
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const selectedStatuses = ref<string[]>(initStatusFilter())
|
2026-03-16 08:38:49 +02:00
|
|
|
|
const selectedCategory = ref('')
|
2026-03-16 10:28:06 +02:00
|
|
|
|
const taskSearch = ref('')
|
2026-03-18 17:34:33 +02:00
|
|
|
|
const dateFrom = ref('')
|
|
|
|
|
|
const dateTo = ref('')
|
2026-03-15 23:22:49 +02:00
|
|
|
|
|
|
|
|
|
|
function toggleStatus(s: string) {
|
|
|
|
|
|
const idx = selectedStatuses.value.indexOf(s)
|
|
|
|
|
|
if (idx >= 0) selectedStatuses.value.splice(idx, 1)
|
|
|
|
|
|
else selectedStatuses.value.push(s)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearStatusFilter() {
|
|
|
|
|
|
selectedStatuses.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
const decisionTypeFilter = ref('')
|
|
|
|
|
|
const decisionSearch = ref('')
|
|
|
|
|
|
|
2026-03-15 17:35:08 +02:00
|
|
|
|
// Auto/Review mode (persisted per project)
|
|
|
|
|
|
const autoMode = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
function loadMode() {
|
2026-03-15 20:02:01 +02:00
|
|
|
|
if (project.value?.execution_mode) {
|
2026-03-16 10:14:24 +02:00
|
|
|
|
autoMode.value = project.value.execution_mode === 'auto_complete'
|
2026-03-15 20:02:01 +02:00
|
|
|
|
} else {
|
2026-03-16 10:14:24 +02:00
|
|
|
|
autoMode.value = localStorage.getItem(`kin-mode-${props.id}`) === 'auto_complete'
|
2026-03-15 20:02:01 +02:00
|
|
|
|
}
|
2026-03-15 17:35:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 20:02:01 +02:00
|
|
|
|
async function toggleMode() {
|
2026-03-15 17:35:08 +02:00
|
|
|
|
autoMode.value = !autoMode.value
|
2026-03-16 10:14:24 +02:00
|
|
|
|
localStorage.setItem(`kin-mode-${props.id}`, autoMode.value ? 'auto_complete' : 'review')
|
2026-03-15 20:02:01 +02:00
|
|
|
|
try {
|
2026-03-16 10:14:24 +02:00
|
|
|
|
await api.patchProject(props.id, { execution_mode: autoMode.value ? 'auto_complete' : 'review' })
|
|
|
|
|
|
if (project.value) project.value = { ...project.value, execution_mode: autoMode.value ? 'auto_complete' : 'review' }
|
2026-03-15 20:02:01 +02:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
error.value = e.message
|
|
|
|
|
|
}
|
2026-03-15 17:35:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 07:06:34 +02:00
|
|
|
|
// Autocommit toggle
|
|
|
|
|
|
const autocommit = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
function loadAutocommit() {
|
|
|
|
|
|
autocommit.value = !!(project.value?.autocommit_enabled)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function toggleAutocommit() {
|
|
|
|
|
|
autocommit.value = !autocommit.value
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.patchProject(props.id, { autocommit_enabled: autocommit.value })
|
|
|
|
|
|
if (project.value) project.value = { ...project.value, autocommit_enabled: autocommit.value ? 1 : 0 }
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
error.value = e.message
|
|
|
|
|
|
autocommit.value = !autocommit.value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 16:02:19 +02:00
|
|
|
|
// Auto-test toggle
|
|
|
|
|
|
const autoTest = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
function loadAutoTest() {
|
|
|
|
|
|
autoTest.value = !!(project.value?.auto_test_enabled)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function toggleAutoTest() {
|
|
|
|
|
|
autoTest.value = !autoTest.value
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.patchProject(props.id, { auto_test_enabled: autoTest.value })
|
|
|
|
|
|
if (project.value) project.value = { ...project.value, auto_test_enabled: autoTest.value ? 1 : 0 }
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
error.value = e.message
|
|
|
|
|
|
autoTest.value = !autoTest.value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 20:54:20 +02:00
|
|
|
|
// Worktrees toggle
|
|
|
|
|
|
const worktrees = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
function loadWorktrees() {
|
|
|
|
|
|
worktrees.value = !!(project.value?.worktrees_enabled)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function toggleWorktrees() {
|
|
|
|
|
|
worktrees.value = !worktrees.value
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.patchProject(props.id, { worktrees_enabled: worktrees.value })
|
|
|
|
|
|
if (project.value) project.value = { ...project.value, worktrees_enabled: worktrees.value ? 1 : 0 }
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
error.value = e.message
|
|
|
|
|
|
worktrees.value = !worktrees.value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 14:30:36 +02:00
|
|
|
|
// Settings form
|
|
|
|
|
|
const settingsForm = ref({
|
|
|
|
|
|
execution_mode: 'review',
|
|
|
|
|
|
autocommit_enabled: false,
|
|
|
|
|
|
auto_test_enabled: false,
|
|
|
|
|
|
worktrees_enabled: false,
|
|
|
|
|
|
test_command: '',
|
|
|
|
|
|
deploy_host: '',
|
|
|
|
|
|
deploy_path: '',
|
|
|
|
|
|
deploy_runtime: '',
|
|
|
|
|
|
deploy_restart_cmd: '',
|
|
|
|
|
|
deploy_command: '',
|
|
|
|
|
|
obsidian_vault_path: '',
|
|
|
|
|
|
ssh_host: '',
|
|
|
|
|
|
ssh_user: '',
|
|
|
|
|
|
ssh_key_path: '',
|
|
|
|
|
|
ssh_proxy_jump: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
const settingsSaving = ref(false)
|
|
|
|
|
|
const settingsSaveStatus = ref('')
|
|
|
|
|
|
const syncingObsidian = ref(false)
|
|
|
|
|
|
const obsidianSyncResult = ref<ObsidianSyncResult | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
function loadSettingsForm() {
|
|
|
|
|
|
if (!project.value) return
|
|
|
|
|
|
settingsForm.value = {
|
|
|
|
|
|
execution_mode: project.value.execution_mode ?? 'review',
|
|
|
|
|
|
autocommit_enabled: !!(project.value.autocommit_enabled),
|
|
|
|
|
|
auto_test_enabled: !!(project.value.auto_test_enabled),
|
|
|
|
|
|
worktrees_enabled: !!(project.value.worktrees_enabled),
|
|
|
|
|
|
test_command: project.value.test_command ?? '',
|
|
|
|
|
|
deploy_host: project.value.deploy_host ?? '',
|
|
|
|
|
|
deploy_path: project.value.deploy_path ?? '',
|
|
|
|
|
|
deploy_runtime: project.value.deploy_runtime ?? '',
|
|
|
|
|
|
deploy_restart_cmd: project.value.deploy_restart_cmd ?? '',
|
|
|
|
|
|
deploy_command: project.value.deploy_command ?? '',
|
|
|
|
|
|
obsidian_vault_path: project.value.obsidian_vault_path ?? '',
|
|
|
|
|
|
ssh_host: project.value.ssh_host ?? '',
|
|
|
|
|
|
ssh_user: project.value.ssh_user ?? '',
|
|
|
|
|
|
ssh_key_path: project.value.ssh_key_path ?? '',
|
|
|
|
|
|
ssh_proxy_jump: project.value.ssh_proxy_jump ?? '',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveSettings() {
|
|
|
|
|
|
settingsSaving.value = true
|
|
|
|
|
|
settingsSaveStatus.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await api.patchProject(props.id, {
|
|
|
|
|
|
execution_mode: settingsForm.value.execution_mode,
|
|
|
|
|
|
autocommit_enabled: settingsForm.value.autocommit_enabled,
|
|
|
|
|
|
auto_test_enabled: settingsForm.value.auto_test_enabled,
|
|
|
|
|
|
worktrees_enabled: settingsForm.value.worktrees_enabled,
|
|
|
|
|
|
test_command: settingsForm.value.test_command,
|
|
|
|
|
|
deploy_host: settingsForm.value.deploy_host,
|
|
|
|
|
|
deploy_path: settingsForm.value.deploy_path,
|
|
|
|
|
|
deploy_runtime: settingsForm.value.deploy_runtime,
|
|
|
|
|
|
deploy_restart_cmd: settingsForm.value.deploy_restart_cmd,
|
|
|
|
|
|
deploy_command: settingsForm.value.deploy_command,
|
|
|
|
|
|
obsidian_vault_path: settingsForm.value.obsidian_vault_path,
|
|
|
|
|
|
ssh_host: settingsForm.value.ssh_host,
|
|
|
|
|
|
ssh_user: settingsForm.value.ssh_user,
|
|
|
|
|
|
ssh_key_path: settingsForm.value.ssh_key_path,
|
|
|
|
|
|
ssh_proxy_jump: settingsForm.value.ssh_proxy_jump,
|
|
|
|
|
|
})
|
|
|
|
|
|
if (project.value) {
|
|
|
|
|
|
project.value = { ...project.value, ...updated }
|
|
|
|
|
|
loadMode()
|
|
|
|
|
|
loadAutocommit()
|
|
|
|
|
|
loadAutoTest()
|
|
|
|
|
|
loadWorktrees()
|
|
|
|
|
|
}
|
|
|
|
|
|
settingsSaveStatus.value = t('common.saved')
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
settingsSaveStatus.value = `${t('common.error')}: ${e.message}`
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
settingsSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function syncObsidianVault() {
|
|
|
|
|
|
syncingObsidian.value = true
|
|
|
|
|
|
obsidianSyncResult.value = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.patchProject(props.id, { obsidian_vault_path: settingsForm.value.obsidian_vault_path })
|
|
|
|
|
|
obsidianSyncResult.value = await api.syncObsidian(props.id)
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
settingsSaveStatus.value = `${t('common.error')}: ${e.message}`
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
syncingObsidian.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 17:44:16 +02:00
|
|
|
|
// Audit
|
|
|
|
|
|
const auditLoading = ref(false)
|
|
|
|
|
|
const auditResult = ref<AuditResult | null>(null)
|
|
|
|
|
|
const showAuditModal = ref(false)
|
|
|
|
|
|
const auditApplying = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
async function runAudit() {
|
|
|
|
|
|
auditLoading.value = true
|
|
|
|
|
|
auditResult.value = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await api.auditProject(props.id)
|
|
|
|
|
|
auditResult.value = res
|
|
|
|
|
|
showAuditModal.value = true
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
error.value = e.message
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
auditLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function applyAudit() {
|
|
|
|
|
|
if (!auditResult.value?.already_done?.length) return
|
|
|
|
|
|
auditApplying.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const ids = auditResult.value.already_done.map(t => t.id)
|
|
|
|
|
|
await api.auditApply(props.id, ids)
|
|
|
|
|
|
showAuditModal.value = false
|
|
|
|
|
|
auditResult.value = null
|
|
|
|
|
|
await load()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
error.value = e.message
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
auditApplying.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 19:26:51 +02:00
|
|
|
|
// 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) {
|
2026-03-18 07:57:15 +02:00
|
|
|
|
if (!confirm(t('projectView.delete_env_confirm'))) return
|
2026-03-16 19:26:51 +02:00
|
|
|
|
try {
|
|
|
|
|
|
await api.deleteEnvironment(props.id, envId)
|
|
|
|
|
|
await loadEnvironments()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
envsError.value = e.message
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 17:39:40 +02:00
|
|
|
|
// 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)
|
2026-03-17 18:37:14 +02:00
|
|
|
|
const linkForm = ref({ to_project: '', type: 'depends_on', description: '' })
|
2026-03-17 17:39:40 +02:00
|
|
|
|
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 = ''
|
2026-03-18 07:57:15 +02:00
|
|
|
|
if (!linkForm.value.to_project) { linkFormError.value = t('projectView.select_project'); return }
|
2026-03-17 17:39:40 +02:00
|
|
|
|
linkSaving.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.createProjectLink({
|
|
|
|
|
|
from_project: props.id,
|
|
|
|
|
|
to_project: linkForm.value.to_project,
|
2026-03-17 18:41:21 +02:00
|
|
|
|
type: linkForm.value.type,
|
2026-03-17 17:39:40 +02:00
|
|
|
|
description: linkForm.value.description || undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
showAddLink.value = false
|
2026-03-17 18:41:21 +02:00
|
|
|
|
linkForm.value = { to_project: '', type: 'depends_on', description: '' }
|
2026-03-17 17:39:40 +02:00
|
|
|
|
await loadLinks()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
linkFormError.value = e.message
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
linkSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function deleteLink(id: number) {
|
2026-03-18 07:57:15 +02:00
|
|
|
|
if (!confirm(t('projectView.delete_link_confirm'))) return
|
2026-03-17 17:39:40 +02:00
|
|
|
|
try {
|
|
|
|
|
|
await api.deleteProjectLink(id)
|
|
|
|
|
|
await loadLinks()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
linksError.value = e.message
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const allProjects = ref<{ id: string; name: string }[]>([])
|
|
|
|
|
|
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
// Add task modal
|
2026-03-16 08:34:30 +02:00
|
|
|
|
const TASK_CATEGORIES = ['SEC', 'UI', 'API', 'INFRA', 'BIZ', 'DB', 'ARCH', 'TEST', 'PERF', 'DOCS', 'FIX', 'OBS']
|
|
|
|
|
|
const CATEGORY_COLORS: Record<string, string> = {
|
2026-03-16 08:38:49 +02:00
|
|
|
|
SEC: 'red', UI: 'purple', API: 'blue', INFRA: 'orange', BIZ: 'green',
|
|
|
|
|
|
DB: 'yellow', ARCH: 'indigo', TEST: 'cyan', PERF: 'pink', DOCS: 'gray',
|
|
|
|
|
|
FIX: 'rose', OBS: 'teal',
|
2026-03-16 08:34:30 +02:00
|
|
|
|
}
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
const showAddTask = ref(false)
|
2026-03-16 10:04:01 +02:00
|
|
|
|
const taskForm = ref({ title: '', priority: 5, route_type: '', category: '', acceptance_criteria: '' })
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
const taskFormError = ref('')
|
2026-03-17 16:00:54 +02:00
|
|
|
|
const uploadWarning = ref('')
|
2026-03-16 22:35:31 +02:00
|
|
|
|
const pendingFiles = ref<File[]>([])
|
|
|
|
|
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
function onFileSelect(e: Event) {
|
|
|
|
|
|
const input = e.target as HTMLInputElement
|
|
|
|
|
|
if (input.files) {
|
|
|
|
|
|
pendingFiles.value.push(...Array.from(input.files))
|
|
|
|
|
|
input.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeAddTaskModal() {
|
|
|
|
|
|
showAddTask.value = false
|
|
|
|
|
|
pendingFiles.value = []
|
|
|
|
|
|
}
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
|
|
|
|
|
|
// Add decision modal
|
|
|
|
|
|
const showAddDecision = ref(false)
|
|
|
|
|
|
const decForm = ref({ type: 'decision', title: '', description: '', category: '', tags: '' })
|
|
|
|
|
|
const decFormError = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
project.value = await api.project(props.id)
|
2026-03-16 23:34:22 +02:00
|
|
|
|
loadMode()
|
|
|
|
|
|
loadAutocommit()
|
2026-03-17 16:02:19 +02:00
|
|
|
|
loadAutoTest()
|
2026-03-17 20:54:20 +02:00
|
|
|
|
loadWorktrees()
|
2026-03-18 14:30:36 +02:00
|
|
|
|
loadSettingsForm()
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
error.value = e.message
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 23:22:49 +02:00
|
|
|
|
watch(selectedStatuses, (val) => {
|
|
|
|
|
|
localStorage.setItem(`kin-task-statuses-${props.id}`, JSON.stringify(val))
|
|
|
|
|
|
router.replace({ query: { ...route.query, status: val.length ? val.join(',') : undefined } })
|
|
|
|
|
|
}, { deep: true })
|
2026-03-15 20:02:01 +02:00
|
|
|
|
|
2026-03-16 10:28:06 +02:00
|
|
|
|
watch(() => props.id, () => {
|
|
|
|
|
|
taskSearch.value = ''
|
2026-03-16 19:26:51 +02:00
|
|
|
|
environments.value = []
|
|
|
|
|
|
showScanBanner.value = false
|
|
|
|
|
|
scanTaskId.value = null
|
2026-03-17 17:39:40 +02:00
|
|
|
|
links.value = []
|
|
|
|
|
|
deployResult.value = null
|
2026-03-16 10:28:06 +02:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-16 09:13:34 +02:00
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await load()
|
|
|
|
|
|
await loadPhases()
|
2026-03-16 19:26:51 +02:00
|
|
|
|
await loadEnvironments()
|
2026-03-17 17:39:40 +02:00
|
|
|
|
await loadLinks()
|
|
|
|
|
|
try {
|
|
|
|
|
|
const all = await api.projects()
|
|
|
|
|
|
allProjects.value = all.map(p => ({ id: p.id, name: p.name }))
|
|
|
|
|
|
} catch {}
|
2026-03-18 14:30:36 +02:00
|
|
|
|
if (route.query.tab === 'settings') {
|
|
|
|
|
|
activeTab.value = 'settings'
|
|
|
|
|
|
}
|
2026-03-16 09:13:34 +02:00
|
|
|
|
})
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
if (phasePollTimer) { clearInterval(phasePollTimer); phasePollTimer = null }
|
2026-03-16 10:04:01 +02:00
|
|
|
|
if (kanbanPollTimer) { clearInterval(kanbanPollTimer); kanbanPollTimer = null }
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-16 08:38:49 +02:00
|
|
|
|
const taskCategories = computed(() => {
|
|
|
|
|
|
if (!project.value) return []
|
|
|
|
|
|
const cats = new Set(project.value.tasks.map(t => t.category).filter(Boolean) as string[])
|
|
|
|
|
|
return Array.from(cats).sort()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-21 08:52:40 +02:00
|
|
|
|
const taskStats = computed(() => {
|
|
|
|
|
|
const tasks = project.value?.tasks || []
|
|
|
|
|
|
const total = tasks.length
|
|
|
|
|
|
if (!total) return null
|
|
|
|
|
|
const done = tasks.filter(t => t.status === 'done').length
|
|
|
|
|
|
const running = tasks.filter(t => t.status === 'in_progress').length
|
|
|
|
|
|
const review = tasks.filter(t => t.status === 'review').length
|
|
|
|
|
|
const blocked = tasks.filter(t => t.status === 'blocked').length
|
|
|
|
|
|
const pending = tasks.filter(t => t.status === 'pending').length
|
2026-03-21 09:13:49 +02:00
|
|
|
|
const revising = tasks.filter(t => t.status === 'revising').length
|
|
|
|
|
|
const cancelled = tasks.filter(t => t.status === 'cancelled').length
|
|
|
|
|
|
const decomposed = tasks.filter(t => t.status === 'decomposed').length
|
|
|
|
|
|
return { total, done, running, review, blocked, pending, revising, cancelled, decomposed, pct: Math.round(done / total * 100) }
|
2026-03-21 08:52:40 +02:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-16 10:28:06 +02:00
|
|
|
|
const searchFilteredTasks = computed(() => {
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
if (!project.value) return []
|
2026-03-16 10:28:06 +02:00
|
|
|
|
const q = taskSearch.value.trim().toLowerCase()
|
|
|
|
|
|
if (!q) return project.value.tasks
|
|
|
|
|
|
return project.value.tasks.filter(t => {
|
|
|
|
|
|
if (t.title.toLowerCase().includes(q)) return true
|
|
|
|
|
|
if (t.brief && JSON.stringify(t.brief).toLowerCase().includes(q)) return true
|
|
|
|
|
|
return false
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const filteredTasks = computed(() => {
|
|
|
|
|
|
let tasks = searchFilteredTasks.value
|
2026-03-15 23:22:49 +02:00
|
|
|
|
if (selectedStatuses.value.length > 0) tasks = tasks.filter(t => selectedStatuses.value.includes(t.status))
|
2026-03-16 08:38:49 +02:00
|
|
|
|
if (selectedCategory.value) tasks = tasks.filter(t => t.category === selectedCategory.value)
|
2026-03-18 17:34:33 +02:00
|
|
|
|
if ((dateFrom.value || dateTo.value) && selectedStatuses.value.includes('done')) {
|
|
|
|
|
|
tasks = tasks.filter(t => {
|
|
|
|
|
|
if (t.status !== 'done') return true
|
|
|
|
|
|
const dateStr = (t.completed_at || t.updated_at) ?? ''
|
|
|
|
|
|
const d = dateStr.substring(0, 10)
|
|
|
|
|
|
if (dateFrom.value && d < dateFrom.value) return false
|
|
|
|
|
|
if (dateTo.value && d > dateTo.value) return false
|
|
|
|
|
|
return true
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
return tasks
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-16 07:13:32 +02:00
|
|
|
|
const manualEscalationTasks = computed(() => {
|
|
|
|
|
|
if (!project.value) return []
|
|
|
|
|
|
return project.value.tasks.filter(
|
|
|
|
|
|
t => t.brief?.task_type === 'manual_escalation' && t.status !== 'done' && t.status !== 'cancelled'
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-18 21:14:50 +02:00
|
|
|
|
// Tree helpers
|
|
|
|
|
|
const childrenMap = computed(() => {
|
|
|
|
|
|
const map = new Map<string, Task[]>()
|
|
|
|
|
|
for (const t of (project.value?.tasks || [])) {
|
|
|
|
|
|
if (t.parent_task_id) {
|
|
|
|
|
|
const arr = map.get(t.parent_task_id) || []
|
|
|
|
|
|
arr.push(t)
|
|
|
|
|
|
map.set(t.parent_task_id, arr)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return map
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function taskDepth(task: Task): number {
|
|
|
|
|
|
let depth = 0
|
|
|
|
|
|
let current = task
|
|
|
|
|
|
const visited = new Set<string>()
|
|
|
|
|
|
while (current.parent_task_id && !visited.has(current.id)) {
|
|
|
|
|
|
visited.add(current.id)
|
|
|
|
|
|
const parent = (project.value?.tasks || []).find(t => t.id === current.parent_task_id)
|
|
|
|
|
|
if (!parent) break
|
|
|
|
|
|
current = parent
|
|
|
|
|
|
depth++
|
|
|
|
|
|
}
|
|
|
|
|
|
return depth
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const expandedTasks = ref(new Set<string>())
|
|
|
|
|
|
|
|
|
|
|
|
function toggleExpand(taskId: string) {
|
|
|
|
|
|
const next = new Set(expandedTasks.value)
|
|
|
|
|
|
if (next.has(taskId)) next.delete(taskId)
|
|
|
|
|
|
else next.add(taskId)
|
|
|
|
|
|
expandedTasks.value = next
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hasChildren(taskId: string): boolean {
|
|
|
|
|
|
return (childrenMap.value.get(taskId)?.length || 0) > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rootFilteredTasks = computed(() => {
|
|
|
|
|
|
const taskIds = new Set((project.value?.tasks || []).map(t => t.id))
|
|
|
|
|
|
return filteredTasks.value.filter(t => {
|
|
|
|
|
|
if (!t.parent_task_id) return true
|
|
|
|
|
|
return !taskIds.has(t.parent_task_id)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const flattenedTasks = computed(() => {
|
|
|
|
|
|
const result: Task[] = []
|
2026-03-18 21:26:40 +02:00
|
|
|
|
const visitedInFlatten = new Set<string>()
|
2026-03-18 21:14:50 +02:00
|
|
|
|
function addWithChildren(task: Task) {
|
2026-03-18 21:26:40 +02:00
|
|
|
|
if (visitedInFlatten.has(task.id)) return
|
|
|
|
|
|
visitedInFlatten.add(task.id)
|
2026-03-18 21:14:50 +02:00
|
|
|
|
result.push(task)
|
|
|
|
|
|
if (expandedTasks.value.has(task.id)) {
|
|
|
|
|
|
const children = childrenMap.value.get(task.id) || []
|
|
|
|
|
|
for (const child of children) {
|
|
|
|
|
|
addWithChildren(child)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const t of rootFilteredTasks.value) {
|
|
|
|
|
|
addWithChildren(t)
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
})
|
|
|
|
|
|
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
const filteredDecisions = computed(() => {
|
|
|
|
|
|
if (!project.value) return []
|
|
|
|
|
|
let decs = project.value.decisions
|
|
|
|
|
|
if (decisionTypeFilter.value) decs = decs.filter(d => d.type === decisionTypeFilter.value)
|
|
|
|
|
|
if (decisionSearch.value) {
|
|
|
|
|
|
const q = decisionSearch.value.toLowerCase()
|
|
|
|
|
|
decs = decs.filter(d => d.title.toLowerCase().includes(q) || d.description.toLowerCase().includes(q))
|
|
|
|
|
|
}
|
|
|
|
|
|
return decs
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function taskStatusColor(s: string) {
|
|
|
|
|
|
const m: Record<string, string> = {
|
|
|
|
|
|
pending: 'gray', in_progress: 'blue', review: 'purple',
|
2026-03-15 18:22:17 +02:00
|
|
|
|
done: 'green', blocked: 'red', decomposed: 'yellow', cancelled: 'gray',
|
2026-03-16 22:35:31 +02:00
|
|
|
|
revising: 'orange',
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
}
|
|
|
|
|
|
return m[s] || 'gray'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 21:47:29 +02:00
|
|
|
|
function taskStatusLabel(s: string) {
|
|
|
|
|
|
if (s === 'revising') return t('projectView.status_revising')
|
|
|
|
|
|
return s
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
function decTypeColor(t: string) {
|
|
|
|
|
|
const m: Record<string, string> = {
|
|
|
|
|
|
decision: 'blue', gotcha: 'red', workaround: 'yellow',
|
|
|
|
|
|
rejected_approach: 'gray', convention: 'purple',
|
|
|
|
|
|
}
|
|
|
|
|
|
return m[t] || 'gray'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function modTypeColor(t: string) {
|
|
|
|
|
|
const m: Record<string, string> = {
|
|
|
|
|
|
frontend: 'blue', backend: 'green', shared: 'purple', infra: 'orange',
|
|
|
|
|
|
}
|
|
|
|
|
|
return m[t] || 'gray'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const decTypes = computed(() => {
|
|
|
|
|
|
if (!project.value) return []
|
|
|
|
|
|
const s = new Set(project.value.decisions.map(d => d.type))
|
|
|
|
|
|
return Array.from(s).sort()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
async function addTask() {
|
|
|
|
|
|
taskFormError.value = ''
|
|
|
|
|
|
try {
|
2026-03-16 22:35:31 +02:00
|
|
|
|
const task = await api.createTask({
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
project_id: props.id,
|
|
|
|
|
|
title: taskForm.value.title,
|
|
|
|
|
|
priority: taskForm.value.priority,
|
|
|
|
|
|
route_type: taskForm.value.route_type || undefined,
|
2026-03-16 08:34:30 +02:00
|
|
|
|
category: taskForm.value.category || undefined,
|
2026-03-16 10:04:01 +02:00
|
|
|
|
acceptance_criteria: taskForm.value.acceptance_criteria || undefined,
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
})
|
2026-03-16 22:35:31 +02:00
|
|
|
|
if (pendingFiles.value.length > 0) {
|
|
|
|
|
|
const failedFiles: string[] = []
|
|
|
|
|
|
for (const file of pendingFiles.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.uploadAttachment(task.id, file)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
failedFiles.push(file.name)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
pendingFiles.value = []
|
|
|
|
|
|
if (failedFiles.length > 0) {
|
2026-03-17 16:00:54 +02:00
|
|
|
|
uploadWarning.value = `Не удалось загрузить файлы: ${failedFiles.join(', ')}. Вы можете дозагрузить их через TaskDetail.`
|
2026-03-16 22:35:31 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
showAddTask.value = false
|
2026-03-16 10:04:01 +02:00
|
|
|
|
taskForm.value = { title: '', priority: 5, route_type: '', category: '', acceptance_criteria: '' }
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
await load()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
taskFormError.value = e.message
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 19:26:51 +02:00
|
|
|
|
const runningTaskId = ref<string | null>(null)
|
|
|
|
|
|
|
2026-03-15 15:29:05 +02:00
|
|
|
|
async function runTask(taskId: string, event: Event) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
event.stopPropagation()
|
2026-03-18 07:57:15 +02:00
|
|
|
|
if (!confirm(t('projectView.run_pipeline_confirm', { n: taskId }))) return
|
2026-03-16 19:26:51 +02:00
|
|
|
|
runningTaskId.value = taskId
|
2026-03-15 15:29:05 +02:00
|
|
|
|
try {
|
2026-03-16 23:34:22 +02:00
|
|
|
|
// Sync task execution_mode with current project toggle state before running
|
|
|
|
|
|
await api.patchTask(taskId, { execution_mode: autoMode.value ? 'auto_complete' : 'review' })
|
2026-03-15 23:22:49 +02:00
|
|
|
|
await api.runTask(taskId)
|
2026-03-15 15:29:05 +02:00
|
|
|
|
await load()
|
2026-03-16 10:59:09 +02:00
|
|
|
|
if (activeTab.value === 'kanban') checkAndPollKanban()
|
2026-03-15 15:29:05 +02:00
|
|
|
|
} catch (e: any) {
|
2026-03-16 19:26:51 +02:00
|
|
|
|
if (e instanceof ApiError && e.code === 'task_already_running') {
|
2026-03-18 07:57:15 +02:00
|
|
|
|
error.value = t('projectView.pipeline_already_running')
|
2026-03-16 19:26:51 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
error.value = e.message
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
runningTaskId.value = null
|
2026-03-15 15:29:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 07:13:32 +02:00
|
|
|
|
async function patchTaskField(taskId: string, data: { priority?: number; route_type?: string }) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await api.patchTask(taskId, data)
|
|
|
|
|
|
if (project.value) {
|
|
|
|
|
|
const idx = project.value.tasks.findIndex(t => t.id === taskId)
|
|
|
|
|
|
if (idx >= 0) project.value.tasks[idx] = updated
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
error.value = e.message
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 10:04:01 +02:00
|
|
|
|
// Kanban
|
2026-03-18 07:57:15 +02:00
|
|
|
|
const KANBAN_COLUMNS = computed(() => [
|
|
|
|
|
|
{ status: 'pending', label: t('projectView.kanban_pending'), headerClass: 'text-gray-400', bgClass: 'bg-gray-900/20' },
|
|
|
|
|
|
{ status: 'in_progress', label: t('projectView.kanban_in_progress'), headerClass: 'text-blue-400', bgClass: 'bg-blue-950/20' },
|
|
|
|
|
|
{ status: 'review', label: t('projectView.kanban_review'), headerClass: 'text-purple-400', bgClass: 'bg-purple-950/20' },
|
2026-03-18 21:14:50 +02:00
|
|
|
|
{ status: 'revising', label: t('projectView.kanban_revising'), headerClass: 'text-orange-400', bgClass: 'bg-orange-950/20' },
|
2026-03-18 07:57:15 +02:00
|
|
|
|
{ status: 'blocked', label: t('projectView.kanban_blocked'), headerClass: 'text-red-400', bgClass: 'bg-red-950/20' },
|
|
|
|
|
|
{ status: 'done', label: t('projectView.kanban_done'), headerClass: 'text-green-400', bgClass: 'bg-green-950/20' },
|
|
|
|
|
|
])
|
2026-03-16 10:04:01 +02:00
|
|
|
|
|
|
|
|
|
|
const draggingTaskId = ref<string | null>(null)
|
|
|
|
|
|
const dragOverStatus = ref<string | null>(null)
|
|
|
|
|
|
let kanbanPollTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
|
|
|
|
|
|
|
const kanbanTasksByStatus = computed(() => {
|
|
|
|
|
|
const result: Record<string, Task[]> = {}
|
2026-03-18 07:57:15 +02:00
|
|
|
|
for (const col of KANBAN_COLUMNS.value) result[col.status] = []
|
|
|
|
|
|
for (const task of searchFilteredTasks.value) {
|
|
|
|
|
|
if (result[task.status]) result[task.status].push(task)
|
2026-03-16 10:04:01 +02:00
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function checkAndPollKanban() {
|
|
|
|
|
|
const hasRunning = project.value?.tasks.some(t => t.status === 'in_progress') ?? false
|
|
|
|
|
|
if (hasRunning && !kanbanPollTimer) {
|
|
|
|
|
|
kanbanPollTimer = setInterval(async () => {
|
|
|
|
|
|
project.value = await api.project(props.id).catch(() => project.value)
|
|
|
|
|
|
if (!project.value?.tasks.some(t => t.status === 'in_progress')) {
|
|
|
|
|
|
clearInterval(kanbanPollTimer!)
|
|
|
|
|
|
kanbanPollTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000)
|
|
|
|
|
|
} else if (!hasRunning && kanbanPollTimer) {
|
|
|
|
|
|
clearInterval(kanbanPollTimer)
|
|
|
|
|
|
kanbanPollTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(activeTab, (tab) => {
|
|
|
|
|
|
if (tab === 'kanban') {
|
|
|
|
|
|
checkAndPollKanban()
|
|
|
|
|
|
} else if (kanbanPollTimer) {
|
|
|
|
|
|
clearInterval(kanbanPollTimer)
|
|
|
|
|
|
kanbanPollTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function onDragStart(event: DragEvent, taskId: string) {
|
|
|
|
|
|
draggingTaskId.value = taskId
|
|
|
|
|
|
event.dataTransfer?.setData('text/plain', taskId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onDragEnd() {
|
|
|
|
|
|
draggingTaskId.value = null
|
|
|
|
|
|
dragOverStatus.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onDragLeave(event: DragEvent) {
|
|
|
|
|
|
const target = event.currentTarget as HTMLElement
|
|
|
|
|
|
const related = event.relatedTarget as HTMLElement | null
|
|
|
|
|
|
if (!related || !target.contains(related)) dragOverStatus.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function onKanbanDrop(event: DragEvent, newStatus: string) {
|
|
|
|
|
|
dragOverStatus.value = null
|
|
|
|
|
|
const taskId = event.dataTransfer?.getData('text/plain') || draggingTaskId.value
|
|
|
|
|
|
draggingTaskId.value = null
|
|
|
|
|
|
if (!taskId || !project.value) return
|
|
|
|
|
|
const task = project.value.tasks.find(t => t.id === taskId)
|
|
|
|
|
|
if (!task || task.status === newStatus) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await api.patchTask(taskId, { status: newStatus })
|
|
|
|
|
|
const idx = project.value.tasks.findIndex(t => t.id === taskId)
|
|
|
|
|
|
if (idx >= 0) project.value.tasks[idx] = updated
|
|
|
|
|
|
checkAndPollKanban()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
error.value = e.message
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
async function addDecision() {
|
|
|
|
|
|
decFormError.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const tags = decForm.value.tags ? decForm.value.tags.split(',').map(s => s.trim()).filter(Boolean) : undefined
|
2026-03-16 07:13:32 +02:00
|
|
|
|
await api.createDecision({
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
project_id: props.id,
|
|
|
|
|
|
type: decForm.value.type,
|
|
|
|
|
|
title: decForm.value.title,
|
|
|
|
|
|
description: decForm.value.description,
|
|
|
|
|
|
category: decForm.value.category || undefined,
|
|
|
|
|
|
tags,
|
|
|
|
|
|
})
|
|
|
|
|
|
showAddDecision.value = false
|
|
|
|
|
|
decForm.value = { type: 'decision', title: '', description: '', category: '', tags: '' }
|
|
|
|
|
|
await load()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
decFormError.value = e.message
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-03-18 07:57:15 +02:00
|
|
|
|
<div v-if="loading" class="text-gray-500 text-sm">{{ t('common.loading') }}</div>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<div v-else-if="error" class="text-red-400 text-sm">{{ error }}</div>
|
|
|
|
|
|
<div v-else-if="project">
|
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
|
<div class="mb-6">
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-1">
|
2026-03-18 07:57:15 +02:00
|
|
|
|
<router-link to="/" class="text-gray-600 hover:text-gray-400 text-sm no-underline">{{ t('projectView.back') }}</router-link>
|
2026-03-16 19:26:51 +02:00
|
|
|
|
<span class="text-gray-700">|</span>
|
2026-03-18 07:57:15 +02:00
|
|
|
|
<router-link :to="`/chat/${project.id}`" class="text-indigo-500 hover:text-indigo-400 text-sm no-underline">{{ t('projectView.chat') }}</router-link>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
</div>
|
2026-03-17 17:40:21 +02:00
|
|
|
|
<div class="flex items-center gap-3 mb-2 flex-wrap">
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<h1 class="text-xl font-bold text-gray-100">{{ project.id }}</h1>
|
|
|
|
|
|
<span class="text-gray-400">{{ project.name }}</span>
|
|
|
|
|
|
<Badge :text="project.status" :color="project.status === 'active' ? 'green' : 'gray'" />
|
2026-03-16 09:13:34 +02:00
|
|
|
|
<Badge v-if="project.project_type && project.project_type !== 'development'"
|
|
|
|
|
|
:text="project.project_type"
|
|
|
|
|
|
:color="project.project_type === 'operations' ? 'orange' : 'green'" />
|
2026-03-17 17:40:21 +02:00
|
|
|
|
<button
|
|
|
|
|
|
@click="runDeploy"
|
|
|
|
|
|
:disabled="deploying || !hasDeployConfig"
|
|
|
|
|
|
:title="hasDeployConfig ? 'Deploy project' : 'Настройте deploy-параметры в Settings'"
|
|
|
|
|
|
class="px-3 py-1 text-xs bg-teal-900/50 text-teal-400 border border-teal-800 rounded hover:bg-teal-900 disabled:opacity-50 ml-auto"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span v-if="deploying" class="inline-block w-3 h-3 border-2 border-teal-400 border-t-transparent rounded-full animate-spin mr-1"></span>
|
2026-03-18 07:57:15 +02:00
|
|
|
|
{{ deploying ? t('taskDetail.deploying') : t('projectView.deploy') }}
|
2026-03-17 17:40:21 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Deploy result -->
|
|
|
|
|
|
<div v-if="deployResult" class="mb-3 p-3 rounded border text-xs font-mono"
|
|
|
|
|
|
:class="deployResult.overall_success !== false && deployResult.success ? 'border-teal-800 bg-teal-950/30 text-teal-300' : 'border-red-800 bg-red-950/30 text-red-300'">
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-1">
|
|
|
|
|
|
<span :class="deployResult.overall_success !== false && deployResult.success ? 'text-teal-400' : 'text-red-400'" class="font-semibold">
|
2026-03-18 07:57:15 +02:00
|
|
|
|
{{ deployResult.overall_success !== false && deployResult.success ? t('taskDetail.deploy_succeeded') : t('taskDetail.deploy_failed') }}
|
2026-03-17 17:40:21 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
<span class="text-gray-500">{{ deployResult.duration_seconds }}s</span>
|
|
|
|
|
|
<button @click.stop="deployResult = null" class="ml-auto text-gray-600 hover:text-gray-400 bg-transparent border-none cursor-pointer text-xs">x</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- Structured steps -->
|
|
|
|
|
|
<div v-if="deployResult.results?.length" class="space-y-1 mt-1">
|
|
|
|
|
|
<details v-for="step in deployResult.results" :key="step.step" class="border border-gray-700 rounded">
|
|
|
|
|
|
<summary class="flex items-center gap-2 px-2 py-1 cursor-pointer list-none">
|
|
|
|
|
|
<span :class="step.exit_code === 0 ? 'text-teal-400' : 'text-red-400'" class="font-semibold text-[10px]">{{ step.exit_code === 0 ? 'ok' : 'fail' }}</span>
|
|
|
|
|
|
<span class="text-gray-300 text-[11px]">{{ step.step }}</span>
|
|
|
|
|
|
<span class="text-gray-600 text-[10px] ml-auto">exit {{ step.exit_code }}</span>
|
|
|
|
|
|
</summary>
|
|
|
|
|
|
<div class="px-2 pb-2">
|
|
|
|
|
|
<pre v-if="step.stdout" class="whitespace-pre-wrap text-gray-300 max-h-32 overflow-y-auto text-[10px]">{{ step.stdout }}</pre>
|
|
|
|
|
|
<pre v-if="step.stderr" class="whitespace-pre-wrap text-red-400/80 max-h-32 overflow-y-auto text-[10px] mt-1">{{ step.stderr }}</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</details>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- Legacy output -->
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<pre v-if="deployResult.stdout" class="whitespace-pre-wrap text-gray-300 max-h-40 overflow-y-auto">{{ deployResult.stdout }}</pre>
|
|
|
|
|
|
<pre v-if="deployResult.stderr" class="whitespace-pre-wrap text-red-400/80 max-h-40 overflow-y-auto mt-1">{{ deployResult.stderr }}</pre>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<!-- Dependents -->
|
|
|
|
|
|
<div v-if="deployResult.dependents_deployed?.length" class="mt-2 border-t border-gray-700 pt-2">
|
2026-03-18 07:57:15 +02:00
|
|
|
|
<p class="text-xs text-gray-400 font-semibold mb-1">{{ t('taskDetail.dependent_projects') }}</p>
|
2026-03-17 20:32:49 +02:00
|
|
|
|
<div v-for="dep in deployResult.dependents_deployed" :key="dep" class="flex items-center gap-2 px-2 py-0.5">
|
|
|
|
|
|
<span class="text-teal-400 font-semibold text-[10px]">ok</span>
|
|
|
|
|
|
<span class="text-gray-300 text-[11px]">{{ dep }}</span>
|
|
|
|
|
|
</div>
|
2026-03-17 17:40:21 +02:00
|
|
|
|
</div>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex gap-2 flex-wrap mb-2" v-if="project.tech_stack?.length">
|
|
|
|
|
|
<Badge v-for="t in project.tech_stack" :key="t" :text="t" color="purple" />
|
|
|
|
|
|
</div>
|
2026-03-16 09:13:34 +02:00
|
|
|
|
<!-- Path (development / research) -->
|
|
|
|
|
|
<p v-if="project.project_type !== 'operations'" class="text-xs text-gray-600">{{ project.path }}</p>
|
|
|
|
|
|
<!-- SSH info (operations) -->
|
|
|
|
|
|
<div v-if="project.project_type === 'operations'" class="flex flex-wrap gap-3 text-xs text-gray-600">
|
|
|
|
|
|
<span v-if="project.ssh_host">
|
|
|
|
|
|
<span class="text-gray-500">SSH:</span>
|
|
|
|
|
|
<span class="text-orange-400 ml-1">{{ project.ssh_user || 'root' }}@{{ project.ssh_host }}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-if="project.ssh_key_path">key: {{ project.ssh_key_path }}</span>
|
|
|
|
|
|
<span v-if="project.ssh_proxy_jump">via: {{ project.ssh_proxy_jump }}</span>
|
|
|
|
|
|
</div>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-17 16:01:40 +02:00
|
|
|
|
<!-- Upload warning banner -->
|
|
|
|
|
|
<div v-if="uploadWarning" class="mb-4 px-4 py-3 border border-yellow-700 bg-yellow-950/30 rounded flex items-start justify-between gap-2">
|
|
|
|
|
|
<p class="text-sm text-yellow-300">⚠ {{ uploadWarning }}</p>
|
|
|
|
|
|
<button @click="uploadWarning = ''" class="text-gray-600 hover:text-gray-400 bg-transparent border-none cursor-pointer text-xs shrink-0">✕</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<!-- Tabs -->
|
2026-03-17 17:40:21 +02:00
|
|
|
|
<div class="flex gap-1 mb-4 border-b border-gray-800 flex-wrap">
|
2026-03-18 14:30:36 +02:00
|
|
|
|
<button v-for="tab in (['tasks', 'phases', 'decisions', 'modules', 'kanban', 'environments', 'links', 'settings'] as const)" :key="tab"
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
@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'">
|
2026-03-18 14:30:36 +02:00
|
|
|
|
{{ tab === 'tasks' ? t('projectView.tasks_tab') : tab === 'phases' ? t('projectView.phases_tab') : tab === 'decisions' ? t('projectView.decisions_tab') : tab === 'modules' ? t('projectView.modules_tab') : tab === 'kanban' ? t('projectView.kanban_tab') : tab === 'environments' ? t('projectView.environments') : tab === 'links' ? t('projectView.links_tab') : t('projectView.settings_tab') }}
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<span class="text-xs text-gray-600 ml-1">
|
|
|
|
|
|
{{ tab === 'tasks' ? project.tasks.length
|
2026-03-16 09:13:34 +02:00
|
|
|
|
: tab === 'phases' ? phases.length
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
: tab === 'decisions' ? project.decisions.length
|
2026-03-16 10:04:01 +02:00
|
|
|
|
: tab === 'modules' ? project.modules.length
|
2026-03-16 19:26:51 +02:00
|
|
|
|
: tab === 'environments' ? environments.length
|
2026-03-17 17:40:21 +02:00
|
|
|
|
: tab === 'links' ? links.length
|
2026-03-18 14:30:36 +02:00
|
|
|
|
: tab === 'kanban' ? project.tasks.length
|
|
|
|
|
|
: '' }}
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Tasks Tab -->
|
|
|
|
|
|
<div v-if="activeTab === 'tasks'">
|
2026-03-21 08:52:40 +02:00
|
|
|
|
<!-- Progress stats bar -->
|
|
|
|
|
|
<div v-if="taskStats && taskStats.total > 0" class="mb-3 px-3 py-2 border border-gray-800 rounded-lg bg-gray-900/30">
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-1.5">
|
|
|
|
|
|
<div class="flex-1 h-1.5 bg-gray-800 rounded-full overflow-hidden flex">
|
|
|
|
|
|
<div class="h-full bg-green-700 transition-all rounded-l-full"
|
|
|
|
|
|
:style="{ width: `${taskStats.pct}%` }"></div>
|
|
|
|
|
|
<div v-if="taskStats.running" class="h-full bg-blue-500 animate-pulse transition-all"
|
|
|
|
|
|
:style="{ width: `${Math.round(taskStats.running / taskStats.total * 100)}%` }"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="text-xs text-gray-500 shrink-0">{{ taskStats.done }}/{{ taskStats.total }} ({{ taskStats.pct }}%)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex gap-3 text-[11px]">
|
|
|
|
|
|
<span v-if="taskStats.running" class="text-blue-400">● {{ taskStats.running }} running</span>
|
|
|
|
|
|
<span v-if="taskStats.review" class="text-yellow-400">⚠ {{ taskStats.review }} review</span>
|
|
|
|
|
|
<span v-if="taskStats.blocked" class="text-red-400">✕ {{ taskStats.blocked }} blocked</span>
|
|
|
|
|
|
<span v-if="taskStats.pending" class="text-gray-500">○ {{ taskStats.pending }} pending</span>
|
2026-03-21 09:13:49 +02:00
|
|
|
|
<span v-if="taskStats.revising" class="text-orange-400">↻ {{ taskStats.revising }} revising</span>
|
|
|
|
|
|
<span v-if="taskStats.cancelled" class="text-gray-500">— {{ taskStats.cancelled }} cancelled</span>
|
|
|
|
|
|
<span v-if="taskStats.decomposed" class="text-gray-500">⊕ {{ taskStats.decomposed }} decomposed</span>
|
2026-03-21 08:52:40 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-16 08:38:49 +02:00
|
|
|
|
<div class="flex flex-col gap-2 mb-3">
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<div class="flex gap-1 flex-wrap items-center">
|
|
|
|
|
|
<button v-for="s in ALL_TASK_STATUSES" :key="s"
|
|
|
|
|
|
:data-status="s"
|
|
|
|
|
|
@click="toggleStatus(s)"
|
|
|
|
|
|
class="px-2 py-0.5 text-xs rounded border transition-colors"
|
|
|
|
|
|
:class="selectedStatuses.includes(s)
|
|
|
|
|
|
? 'bg-blue-900/40 text-blue-300 border-blue-700'
|
|
|
|
|
|
: 'bg-gray-900 text-gray-600 border-gray-800 hover:text-gray-400 hover:border-gray-700'"
|
|
|
|
|
|
>{{ s }}</button>
|
|
|
|
|
|
<button v-if="selectedStatuses.length" data-action="clear-status" @click="clearStatusFilter"
|
|
|
|
|
|
class="px-1.5 py-0.5 text-xs text-gray-600 hover:text-red-400 rounded">✕</button>
|
|
|
|
|
|
</div>
|
2026-03-16 08:40:19 +02:00
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
|
<button @click="toggleMode"
|
|
|
|
|
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
|
|
|
|
|
:class="autoMode
|
|
|
|
|
|
? 'bg-yellow-900/30 text-yellow-400 border-yellow-800 hover:bg-yellow-900/50'
|
|
|
|
|
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
|
|
|
|
|
:title="autoMode ? 'Auto mode: agents can write files' : 'Review mode: agents read-only'">
|
|
|
|
|
|
{{ autoMode ? '🔓 Auto' : '🔒 Review' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="toggleAutocommit"
|
|
|
|
|
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
|
|
|
|
|
:class="autocommit
|
|
|
|
|
|
? 'bg-green-900/30 text-green-400 border-green-800 hover:bg-green-900/50'
|
|
|
|
|
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
|
|
|
|
|
:title="autocommit ? 'Autocommit: on — git commit after pipeline' : 'Autocommit: off'">
|
|
|
|
|
|
{{ autocommit ? '✓ Autocommit' : 'Autocommit' }}
|
|
|
|
|
|
</button>
|
2026-03-17 16:02:47 +02:00
|
|
|
|
<button @click="toggleAutoTest"
|
|
|
|
|
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
|
|
|
|
|
:class="autoTest
|
|
|
|
|
|
? 'bg-blue-900/30 text-blue-400 border-blue-800 hover:bg-blue-900/50'
|
|
|
|
|
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
|
|
|
|
|
:title="autoTest ? 'Auto-test: on — запускать тесты после pipeline' : 'Auto-test: off'">
|
2026-03-18 07:57:15 +02:00
|
|
|
|
{{ autoTest ? '✓ ' + t('projectView.auto_test_label') : t('projectView.auto_test_label') }}
|
2026-03-17 16:02:47 +02:00
|
|
|
|
</button>
|
2026-03-17 20:54:20 +02:00
|
|
|
|
<button @click="toggleWorktrees"
|
|
|
|
|
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
|
|
|
|
|
:class="worktrees
|
|
|
|
|
|
? 'bg-teal-900/30 text-teal-400 border-teal-800 hover:bg-teal-900/50'
|
|
|
|
|
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
2026-03-18 07:57:15 +02:00
|
|
|
|
:title="worktrees ? 'Worktrees: on' : 'Worktrees: off'">
|
|
|
|
|
|
{{ worktrees ? t('projectView.worktrees_on') : t('projectView.worktrees_off') }}
|
2026-03-17 20:54:20 +02:00
|
|
|
|
</button>
|
2026-03-16 08:40:19 +02:00
|
|
|
|
<button @click="runAudit" :disabled="auditLoading"
|
|
|
|
|
|
class="px-2 py-1 text-xs bg-purple-900/30 text-purple-400 border border-purple-800 rounded hover:bg-purple-900/50 disabled:opacity-50"
|
|
|
|
|
|
title="Check which pending tasks are already done">
|
|
|
|
|
|
<span v-if="auditLoading" class="inline-block w-3 h-3 border-2 border-purple-400 border-t-transparent rounded-full animate-spin mr-1"></span>
|
2026-03-18 07:57:15 +02:00
|
|
|
|
{{ auditLoading ? 'Auditing...' : t('projectView.audit_backlog') }}
|
2026-03-16 08:40:19 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="showAddTask = true"
|
|
|
|
|
|
class="px-3 py-1 text-xs bg-gray-800 text-gray-300 border border-gray-700 rounded hover:bg-gray-700">
|
2026-03-18 07:57:15 +02:00
|
|
|
|
{{ t('projectView.add_task') }}
|
2026-03-16 08:40:19 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- Category filter -->
|
|
|
|
|
|
<div v-if="taskCategories.length" class="flex gap-1 flex-wrap items-center">
|
|
|
|
|
|
<button @click="selectedCategory = ''"
|
|
|
|
|
|
class="px-2 py-0.5 text-xs rounded border transition-colors"
|
|
|
|
|
|
:class="!selectedCategory
|
|
|
|
|
|
? 'bg-gray-700/60 text-gray-300 border-gray-600'
|
|
|
|
|
|
: 'bg-gray-900 text-gray-600 border-gray-800 hover:text-gray-400 hover:border-gray-700'"
|
2026-03-18 07:57:15 +02:00
|
|
|
|
>{{ t('projectView.all_statuses') }}</button>
|
2026-03-16 08:40:19 +02:00
|
|
|
|
<button v-for="cat in taskCategories" :key="cat"
|
|
|
|
|
|
@click="selectedCategory = cat"
|
|
|
|
|
|
class="px-2 py-0.5 text-xs rounded border transition-colors"
|
|
|
|
|
|
:class="selectedCategory === cat
|
|
|
|
|
|
? 'opacity-100 ring-1 ring-offset-0'
|
|
|
|
|
|
: 'opacity-60 hover:opacity-100'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Badge :text="cat" :color="CATEGORY_COLORS[cat] || 'gray'" />
|
2026-03-15 17:35:08 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-03-16 10:28:06 +02:00
|
|
|
|
<!-- Search -->
|
|
|
|
|
|
<div class="flex items-center gap-1">
|
2026-03-18 07:57:15 +02:00
|
|
|
|
<input v-model="taskSearch" :placeholder="t('projectView.search_placeholder')"
|
2026-03-16 10:28:06 +02:00
|
|
|
|
class="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300 placeholder-gray-600 w-56 focus:border-gray-500 outline-none" />
|
|
|
|
|
|
<button v-if="taskSearch" @click="taskSearch = ''"
|
|
|
|
|
|
class="text-gray-600 hover:text-red-400 text-xs px-1">✕</button>
|
|
|
|
|
|
</div>
|
2026-03-18 17:34:33 +02:00
|
|
|
|
<!-- Date filter for done tasks -->
|
|
|
|
|
|
<div v-if="selectedStatuses.includes('done')" class="flex items-center gap-2">
|
|
|
|
|
|
<span class="text-xs text-gray-600">{{ t('projectView.done_date_from') }}</span>
|
|
|
|
|
|
<input type="date" v-model="dateFrom" data-testid="date-from"
|
|
|
|
|
|
class="bg-gray-800 border border-gray-700 rounded px-2 py-0.5 text-xs text-gray-300 focus:border-gray-500 outline-none" />
|
|
|
|
|
|
<span class="text-xs text-gray-600">{{ t('projectView.done_date_to') }}</span>
|
|
|
|
|
|
<input type="date" v-model="dateTo" data-testid="date-to"
|
|
|
|
|
|
class="bg-gray-800 border border-gray-700 rounded px-2 py-0.5 text-xs text-gray-300 focus:border-gray-500 outline-none" />
|
|
|
|
|
|
<button v-if="dateFrom || dateTo" @click="dateFrom = ''; dateTo = ''"
|
2026-03-18 18:22:31 +02:00
|
|
|
|
data-testid="date-reset-btn"
|
2026-03-18 17:34:33 +02:00
|
|
|
|
class="text-gray-600 hover:text-red-400 text-xs px-1">✕</button>
|
|
|
|
|
|
</div>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
</div>
|
2026-03-16 07:13:32 +02:00
|
|
|
|
<!-- Manual escalation tasks -->
|
|
|
|
|
|
<div v-if="manualEscalationTasks.length" class="mb-4">
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-2">
|
2026-03-18 07:57:15 +02:00
|
|
|
|
<span class="text-xs font-semibold text-orange-400 uppercase tracking-wide">{{ t('projectView.manual_escalations_warn') }}</span>
|
2026-03-16 07:13:32 +02:00
|
|
|
|
<span class="text-xs text-orange-600">({{ manualEscalationTasks.length }})</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
|
<router-link v-for="t in manualEscalationTasks" :key="t.id"
|
|
|
|
|
|
:to="{ path: `/task/${t.id}`, query: selectedStatuses.length ? { back_status: selectedStatuses.join(',') } : undefined }"
|
|
|
|
|
|
class="flex items-center justify-between px-3 py-2 border border-orange-800/60 bg-orange-950/20 rounded text-sm hover:border-orange-600 no-underline block transition-colors">
|
|
|
|
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
|
|
|
|
<span class="text-gray-500 shrink-0 w-24">{{ t.id }}</span>
|
2026-03-18 21:47:29 +02:00
|
|
|
|
<Badge :text="taskStatusLabel(t.status)" :color="taskStatusColor(t.status)" />
|
2026-03-16 08:34:30 +02:00
|
|
|
|
<Badge v-if="t.category" :text="t.category" :color="CATEGORY_COLORS[t.category] || 'gray'" />
|
2026-03-16 07:13:32 +02:00
|
|
|
|
<span class="text-orange-300 truncate">{{ t.title }}</span>
|
|
|
|
|
|
<span v-if="t.parent_task_id" class="text-[10px] text-gray-600 shrink-0">escalated from {{ t.parent_task_id }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center gap-2 text-xs text-gray-600 shrink-0">
|
|
|
|
|
|
<span v-if="t.brief?.description" class="text-orange-600 truncate max-w-[200px]">{{ t.brief.description }}</span>
|
|
|
|
|
|
<span>pri {{ t.priority }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</router-link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-18 21:14:50 +02:00
|
|
|
|
<div v-if="flattenedTasks.length === 0" class="text-gray-600 text-sm">{{ t('projectView.no_tasks') }}</div>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<div v-else class="space-y-1">
|
2026-03-18 21:14:50 +02:00
|
|
|
|
<div v-for="t in flattenedTasks" :key="t.id" :style="{ paddingLeft: taskDepth(t) * 24 + 'px' }">
|
|
|
|
|
|
<router-link
|
2026-03-15 23:22:49 +02:00
|
|
|
|
:to="{ path: `/task/${t.id}`, query: selectedStatuses.length ? { back_status: selectedStatuses.join(',') } : undefined }"
|
2026-03-17 16:14:35 +02:00
|
|
|
|
class="flex flex-col gap-0.5 px-3 py-2 border border-gray-800 rounded text-sm hover:border-gray-600 no-underline block transition-colors">
|
|
|
|
|
|
<div class="flex items-center justify-between gap-2">
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<div class="flex items-center gap-2 min-w-0">
|
2026-03-18 21:14:50 +02:00
|
|
|
|
<button v-if="hasChildren(t.id)" @click.prevent="toggleExpand(t.id)"
|
|
|
|
|
|
data-testid="task-toggle-children"
|
|
|
|
|
|
class="text-gray-500 hover:text-gray-300 w-4 shrink-0">
|
|
|
|
|
|
{{ expandedTasks.has(t.id) ? '▼' : '▶' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span v-else class="w-4 shrink-0"></span>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<span class="text-gray-500 shrink-0 w-24">{{ t.id }}</span>
|
2026-03-18 21:47:29 +02:00
|
|
|
|
<Badge :text="taskStatusLabel(t.status)" :color="taskStatusColor(t.status)" />
|
2026-03-16 08:34:30 +02:00
|
|
|
|
<Badge v-if="t.category" :text="t.category" :color="CATEGORY_COLORS[t.category] || 'gray'" />
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<span class="text-gray-300 truncate">{{ t.title }}</span>
|
2026-03-16 10:14:24 +02:00
|
|
|
|
<span v-if="t.execution_mode === 'auto_complete'"
|
2026-03-15 20:02:01 +02:00
|
|
|
|
class="text-[10px] px-1 py-0.5 bg-yellow-900/40 text-yellow-400 border border-yellow-800 rounded shrink-0"
|
|
|
|
|
|
title="Auto mode">🔓</span>
|
Add follow-up task generation on approve
When approving a task, PM agent analyzes pipeline output and creates
follow-up tasks automatically (e.g. security audit → 8 fix tasks).
core/followup.py:
generate_followups() — collects pipeline output, runs followup agent,
parses JSON task list, creates tasks with parent_task_id linkage.
Handles: bare arrays, {tasks:[...]} wrappers, invalid JSON, empty.
agents/prompts/followup.md — PM prompt for analyzing results and
creating actionable follow-up tasks with priority from severity.
CLI: kin approve <task_id> [--followup] [--decision "text"]
API: POST /api/tasks/{id}/approve {create_followups: true}
Returns {status, decision, followup_tasks: [...]}
Frontend (TaskDetail approve modal):
- Checkbox "Create follow-up tasks" (default ON)
- Loading state during generation
- Results view: list of created tasks with links to /task/:id
ProjectView: tasks show "from VDOL-001" for follow-ups.
13 new tests (followup), 125 total, all passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 15:02:58 +02:00
|
|
|
|
<span v-if="t.parent_task_id" class="text-[10px] text-gray-600 shrink-0">from {{ t.parent_task_id }}</span>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center gap-2 text-xs text-gray-600 shrink-0">
|
|
|
|
|
|
<span v-if="t.assigned_role">{{ t.assigned_role }}</span>
|
2026-03-16 07:13:32 +02:00
|
|
|
|
<select
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@change.stop="patchTaskField(t.id, { route_type: ($event.target as HTMLSelectElement).value })"
|
|
|
|
|
|
:value="(t.brief as Record<string, string> | null)?.route_type || ''"
|
|
|
|
|
|
class="bg-gray-900 border border-gray-700 rounded px-1 py-0.5 text-[10px] text-gray-500 cursor-pointer hover:border-gray-500 hover:text-gray-300 transition-colors"
|
|
|
|
|
|
title="Task type">
|
|
|
|
|
|
<option value="">—</option>
|
|
|
|
|
|
<option value="debug">debug</option>
|
|
|
|
|
|
<option value="feature">feature</option>
|
|
|
|
|
|
<option value="refactor">refactor</option>
|
|
|
|
|
|
<option value="hotfix">hotfix</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<select
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
@change.stop="patchTaskField(t.id, { priority: Number(($event.target as HTMLSelectElement).value) })"
|
|
|
|
|
|
:value="t.priority"
|
|
|
|
|
|
class="bg-gray-900 border border-gray-700 rounded px-1 py-0.5 text-[10px] text-gray-500 cursor-pointer hover:border-gray-500 hover:text-gray-300 transition-colors"
|
|
|
|
|
|
title="Priority">
|
|
|
|
|
|
<option v-for="n in 10" :key="n" :value="n">p{{ n }}</option>
|
|
|
|
|
|
</select>
|
2026-03-15 15:29:05 +02:00
|
|
|
|
<button v-if="t.status === 'pending'"
|
|
|
|
|
|
@click="runTask(t.id, $event)"
|
2026-03-16 19:26:51 +02:00
|
|
|
|
: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>
|
2026-03-15 15:29:05 +02:00
|
|
|
|
<span v-if="t.status === 'in_progress'"
|
|
|
|
|
|
class="inline-block w-2 h-2 bg-blue-500 rounded-full animate-pulse" title="Running"></span>
|
2026-03-16 22:35:31 +02:00
|
|
|
|
<button v-if="t.status === 'review' || t.status === 'done'"
|
|
|
|
|
|
@click.prevent.stop="openTaskRevise(t.id)"
|
|
|
|
|
|
class="px-2 py-0.5 bg-orange-900/40 text-orange-400 border border-orange-800 rounded hover:bg-orange-900 text-[10px]"
|
|
|
|
|
|
title="Отправить на доработку">
|
|
|
|
|
|
↩ Revise
|
|
|
|
|
|
</button>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
</div>
|
2026-03-17 16:14:35 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="t.status === 'blocked' && t.blocked_reason" class="text-xs text-red-400 truncate">{{ t.blocked_reason }}</div>
|
Add task detail view, pipeline visualization, approve/reject workflow
API (web/api.py) — 5 new endpoints:
GET /api/tasks/{id}/pipeline — agent_logs as pipeline steps
GET /api/tasks/{id}/full — task + steps + related decisions
POST /api/tasks/{id}/approve — mark done, optionally add decision
POST /api/tasks/{id}/reject — return to pending with reason
POST /api/tasks/{id}/run — launch pipeline in background (202)
Frontend:
TaskDetail (/task/:id) — full task page with:
- Pipeline graph: role cards with icons, arrows, status colors
- Click step → expand output (pre-formatted, JSON detected)
- Action bar: Approve (with optional decision), Reject, Run Pipeline
- Polling for live pipeline updates
Dashboard: review_tasks badge ("awaiting review" in yellow)
ProjectView: task rows are now clickable links to /task/:id
Runner: output_summary no longer truncated (full output for GUI).
Models: get_project_summary includes review_tasks count.
13 new API tests, 105 total, all passing. Frontend builds clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:32:29 +02:00
|
|
|
|
</router-link>
|
2026-03-18 21:14:50 +02:00
|
|
|
|
</div>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-16 09:13:34 +02:00
|
|
|
|
<!-- Phases Tab -->
|
|
|
|
|
|
<div v-if="activeTab === 'phases'">
|
2026-03-16 15:48:09 +02:00
|
|
|
|
<div v-if="claudeLoginError" class="mb-3 px-4 py-3 border border-yellow-700 bg-yellow-950/30 rounded">
|
|
|
|
|
|
<div class="flex items-start justify-between gap-2">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="text-sm font-semibold text-yellow-300">⚠ Claude CLI requires login</p>
|
|
|
|
|
|
<p class="text-xs text-yellow-200/80 mt-1">Откройте терминал и выполните:</p>
|
|
|
|
|
|
<code class="text-xs text-yellow-400 font-mono bg-black/30 px-2 py-0.5 rounded mt-1 inline-block">claude login</code>
|
|
|
|
|
|
<p class="text-xs text-gray-500 mt-1">После входа повторите запуск pipeline.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button @click="claudeLoginError = false" class="text-gray-600 hover:text-gray-400 bg-transparent border-none cursor-pointer text-xs shrink-0">✕</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-18 07:57:15 +02:00
|
|
|
|
<p v-if="phasesLoading" class="text-gray-500 text-sm">{{ t('projectView.loading_phases') }}</p>
|
2026-03-16 09:13:34 +02:00
|
|
|
|
<p v-else-if="phaseError" class="text-red-400 text-sm">{{ phaseError }}</p>
|
|
|
|
|
|
<div v-else-if="phases.length === 0" class="text-gray-600 text-sm">
|
|
|
|
|
|
No research phases. Use "New Project" to start a research workflow.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="space-y-2">
|
|
|
|
|
|
<div v-for="ph in phases" :key="ph.id"
|
|
|
|
|
|
class="px-4 py-3 border rounded transition-colors"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
'border-blue-700 bg-blue-950/20': ph.status === 'active',
|
|
|
|
|
|
'border-green-800 bg-green-950/10': ph.status === 'approved',
|
|
|
|
|
|
'border-red-800 bg-red-950/10': ph.status === 'rejected',
|
|
|
|
|
|
'border-yellow-700 bg-yellow-950/10': ph.status === 'revising',
|
|
|
|
|
|
'border-gray-800': ph.status === 'pending',
|
|
|
|
|
|
}">
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<span class="text-xs text-gray-600 w-5 text-right">{{ ph.phase_order + 1 }}.</span>
|
|
|
|
|
|
<Badge :text="ph.status" :color="phaseStatusColor(ph.status)" />
|
|
|
|
|
|
<span class="text-sm font-medium text-gray-200">{{ ph.role.replace(/_/g, ' ') }}</span>
|
|
|
|
|
|
<span v-if="ph.revise_count" class="text-xs text-yellow-500">(revise ×{{ ph.revise_count }})</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<router-link v-if="ph.task_id"
|
|
|
|
|
|
:to="`/task/${ph.task_id}`"
|
|
|
|
|
|
class="text-xs text-blue-400 hover:text-blue-300 no-underline">
|
|
|
|
|
|
{{ ph.task_id }}
|
|
|
|
|
|
</router-link>
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
<!-- Running indicator -->
|
|
|
|
|
|
<span v-if="ph.task?.status === 'in_progress'"
|
|
|
|
|
|
class="inline-block w-2 h-2 bg-blue-500 rounded-full animate-pulse"
|
|
|
|
|
|
title="Agent running..."></span>
|
|
|
|
|
|
<!-- Start Research button: phase ready but task not started yet -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="(ph.status === 'active' || ph.status === 'revising') && ph.task?.status === 'pending'"
|
|
|
|
|
|
@click="startPhase"
|
|
|
|
|
|
:disabled="startPhaseSaving"
|
|
|
|
|
|
class="px-2 py-0.5 text-xs bg-blue-900/40 text-blue-400 border border-blue-800 rounded hover:bg-blue-900 disabled:opacity-50">
|
|
|
|
|
|
{{ startPhaseSaving ? 'Starting...' : (ph.status === 'revising' ? 'Re-run' : 'Start Research') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<!-- Approve/Revise/Reject: only when agent submitted work for review (decision #78) -->
|
|
|
|
|
|
<template v-if="(ph.status === 'active' || ph.status === 'revising') && ph.task?.status === 'review'">
|
|
|
|
|
|
<button @click="approvePhase(ph.id)" :disabled="approvePhaseSaving"
|
|
|
|
|
|
class="px-2 py-0.5 text-xs bg-green-900/40 text-green-400 border border-green-800 rounded hover:bg-green-900 disabled:opacity-50">
|
|
|
|
|
|
{{ approvePhaseSaving ? '...' : 'Approve' }}
|
2026-03-16 09:13:34 +02:00
|
|
|
|
</button>
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
<button @click="openRevise(ph.id)" :disabled="approvePhaseSaving"
|
|
|
|
|
|
class="px-2 py-0.5 text-xs bg-yellow-900/40 text-yellow-400 border border-yellow-800 rounded hover:bg-yellow-900 disabled:opacity-50">
|
2026-03-16 09:13:34 +02:00
|
|
|
|
Revise
|
|
|
|
|
|
</button>
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
<button @click="openReject(ph.id)" :disabled="approvePhaseSaving"
|
|
|
|
|
|
class="px-2 py-0.5 text-xs bg-red-900/40 text-red-400 border border-red-800 rounded hover:bg-red-900 disabled:opacity-50">
|
2026-03-16 09:13:34 +02:00
|
|
|
|
Reject
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="ph.task?.title" class="mt-1 text-xs text-gray-500 ml-7">{{ ph.task.title }}</div>
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
|
<div v-if="ph.revise_comment" class="mt-1 text-xs text-yellow-600 ml-7">➤ {{ ph.revise_comment }}</div>
|
2026-03-16 09:13:34 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Revise Modal -->
|
|
|
|
|
|
<Modal v-if="showReviseModal" title="Request Revision" @close="showReviseModal = false">
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
<textarea v-model="reviseComment" placeholder="Что нужно доработать?" 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>
|
|
|
|
|
|
<p v-if="reviseError" class="text-red-400 text-xs">{{ reviseError }}</p>
|
|
|
|
|
|
<button @click="submitRevise" :disabled="reviseSaving"
|
|
|
|
|
|
class="w-full py-2 bg-yellow-900/50 text-yellow-400 border border-yellow-800 rounded text-sm hover:bg-yellow-900 disabled:opacity-50">
|
|
|
|
|
|
{{ reviseSaving ? 'Saving...' : 'Send for Revision' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Reject Modal -->
|
|
|
|
|
|
<Modal v-if="showRejectModal" title="Reject Phase" @close="showRejectModal = false">
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
<textarea v-model="rejectReason" placeholder="Причина отклонения" rows="3"
|
|
|
|
|
|
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>
|
|
|
|
|
|
<p v-if="rejectError" class="text-red-400 text-xs">{{ rejectError }}</p>
|
|
|
|
|
|
<button @click="submitReject" :disabled="rejectSaving"
|
|
|
|
|
|
class="w-full py-2 bg-red-900/50 text-red-400 border border-red-800 rounded text-sm hover:bg-red-900 disabled:opacity-50">
|
|
|
|
|
|
{{ rejectSaving ? 'Saving...' : 'Reject' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<!-- Decisions Tab -->
|
|
|
|
|
|
<div v-if="activeTab === 'decisions'">
|
|
|
|
|
|
<div class="flex items-center justify-between mb-3">
|
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
|
<select v-model="decisionTypeFilter"
|
|
|
|
|
|
class="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300">
|
|
|
|
|
|
<option value="">All types</option>
|
|
|
|
|
|
<option v-for="t in decTypes" :key="t" :value="t">{{ t }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<input v-model="decisionSearch" placeholder="Search..."
|
|
|
|
|
|
class="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300 placeholder-gray-600 w-48" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button @click="showAddDecision = true"
|
|
|
|
|
|
class="px-3 py-1 text-xs bg-gray-800 text-gray-300 border border-gray-700 rounded hover:bg-gray-700">
|
|
|
|
|
|
+ Decision
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="filteredDecisions.length === 0" class="text-gray-600 text-sm">No decisions.</div>
|
|
|
|
|
|
<div v-else class="space-y-2">
|
|
|
|
|
|
<div v-for="d in filteredDecisions" :key="d.id"
|
|
|
|
|
|
class="px-3 py-2 border border-gray-800 rounded hover:border-gray-700">
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-1">
|
|
|
|
|
|
<span class="text-gray-600 text-xs">#{{ d.id }}</span>
|
|
|
|
|
|
<Badge :text="d.type" :color="decTypeColor(d.type)" />
|
|
|
|
|
|
<Badge v-if="d.category" :text="d.category" color="gray" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-sm text-gray-300">{{ d.title }}</div>
|
|
|
|
|
|
<div v-if="d.description !== d.title" class="text-xs text-gray-500 mt-1">{{ d.description }}</div>
|
|
|
|
|
|
<div v-if="d.tags?.length" class="flex gap-1 mt-1">
|
|
|
|
|
|
<Badge v-for="tag in d.tags" :key="tag" :text="tag" color="purple" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Modules Tab -->
|
|
|
|
|
|
<div v-if="activeTab === 'modules'">
|
|
|
|
|
|
<div v-if="project.modules.length === 0" class="text-gray-600 text-sm">No modules.</div>
|
|
|
|
|
|
<div v-else class="space-y-1">
|
|
|
|
|
|
<div v-for="m in project.modules" :key="m.id"
|
|
|
|
|
|
class="flex items-center justify-between px-3 py-2 border border-gray-800 rounded text-sm hover:border-gray-700">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<span class="text-gray-300 font-medium">{{ m.name }}</span>
|
|
|
|
|
|
<Badge :text="m.type" :color="modTypeColor(m.type)" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center gap-3 text-xs text-gray-600">
|
|
|
|
|
|
<span>{{ m.path }}</span>
|
|
|
|
|
|
<span v-if="m.owner_role">{{ m.owner_role }}</span>
|
|
|
|
|
|
<span v-if="m.description">{{ m.description }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-16 10:04:01 +02:00
|
|
|
|
<!-- Kanban Tab -->
|
2026-03-16 10:28:06 +02:00
|
|
|
|
<div v-if="activeTab === 'kanban'" class="pb-4">
|
|
|
|
|
|
<div class="flex items-center justify-between gap-2 mb-3">
|
|
|
|
|
|
<div class="flex items-center gap-1">
|
2026-03-18 15:22:17 +02:00
|
|
|
|
<input v-model="taskSearch" :placeholder="t('projectView.kanban_search_placeholder')"
|
2026-03-16 10:28:06 +02:00
|
|
|
|
class="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300 placeholder-gray-600 w-48 focus:border-gray-500 outline-none" />
|
|
|
|
|
|
<button v-if="taskSearch" @click="taskSearch = ''"
|
|
|
|
|
|
class="text-gray-600 hover:text-red-400 text-xs px-1">✕</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
|
<button @click="toggleMode"
|
|
|
|
|
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
|
|
|
|
|
:class="autoMode
|
|
|
|
|
|
? 'bg-yellow-900/30 text-yellow-400 border-yellow-800 hover:bg-yellow-900/50'
|
|
|
|
|
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
|
|
|
|
|
:title="autoMode ? 'Auto mode: agents can write files' : 'Review mode: agents read-only'">
|
|
|
|
|
|
{{ autoMode ? '🔓 Авто' : '🔒 Review' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="toggleAutocommit"
|
|
|
|
|
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
|
|
|
|
|
:class="autocommit
|
|
|
|
|
|
? 'bg-green-900/30 text-green-400 border-green-800 hover:bg-green-900/50'
|
|
|
|
|
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
|
|
|
|
|
:title="autocommit ? 'Autocommit: on — git commit after pipeline' : 'Autocommit: off'">
|
|
|
|
|
|
{{ autocommit ? '✓ Автокомит' : 'Автокомит' }}
|
|
|
|
|
|
</button>
|
2026-03-17 16:02:47 +02:00
|
|
|
|
<button @click="toggleAutoTest"
|
|
|
|
|
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
|
|
|
|
|
:class="autoTest
|
|
|
|
|
|
? 'bg-blue-900/30 text-blue-400 border-blue-800 hover:bg-blue-900/50'
|
|
|
|
|
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
|
|
|
|
|
:title="autoTest ? 'Auto-test: on — запускать тесты после pipeline' : 'Auto-test: off'">
|
2026-03-18 07:57:15 +02:00
|
|
|
|
{{ autoTest ? '✓ ' + t('projectView.auto_test_label') : t('projectView.auto_test_label') }}
|
2026-03-17 16:02:47 +02:00
|
|
|
|
</button>
|
2026-03-17 20:54:20 +02:00
|
|
|
|
<button @click="toggleWorktrees"
|
|
|
|
|
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
|
|
|
|
|
:class="worktrees
|
|
|
|
|
|
? 'bg-teal-900/30 text-teal-400 border-teal-800 hover:bg-teal-900/50'
|
|
|
|
|
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
2026-03-18 07:57:15 +02:00
|
|
|
|
:title="worktrees ? 'Worktrees: on' : 'Worktrees: off'">
|
|
|
|
|
|
{{ worktrees ? t('projectView.worktrees_on') : t('projectView.worktrees_off') }}
|
2026-03-17 20:54:20 +02:00
|
|
|
|
</button>
|
2026-03-16 10:28:06 +02:00
|
|
|
|
<button @click="runAudit" :disabled="auditLoading"
|
|
|
|
|
|
class="px-2 py-1 text-xs bg-purple-900/30 text-purple-400 border border-purple-800 rounded hover:bg-purple-900/50 disabled:opacity-50"
|
|
|
|
|
|
title="Check which pending tasks are already done">
|
|
|
|
|
|
<span v-if="auditLoading" class="inline-block w-3 h-3 border-2 border-purple-400 border-t-transparent rounded-full animate-spin mr-1"></span>
|
2026-03-18 07:57:15 +02:00
|
|
|
|
{{ auditLoading ? 'Auditing...' : t('projectView.audit_backlog') }}
|
2026-03-16 10:28:06 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="showAddTask = true"
|
|
|
|
|
|
class="px-3 py-1 text-xs bg-gray-800 text-gray-300 border border-gray-700 rounded hover:bg-gray-700">
|
2026-03-18 15:22:17 +02:00
|
|
|
|
{{ t('projectView.kanban_add_task') }}
|
2026-03-16 10:28:06 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="overflow-x-auto">
|
2026-03-16 10:59:09 +02:00
|
|
|
|
<div class="flex gap-3 w-full">
|
|
|
|
|
|
<div v-for="col in KANBAN_COLUMNS" :key="col.status" class="flex-1 min-w-[12rem] flex flex-col gap-2">
|
2026-03-16 10:04:01 +02:00
|
|
|
|
<!-- Column header -->
|
|
|
|
|
|
<div class="flex items-center gap-2 px-2 py-1.5">
|
|
|
|
|
|
<span class="text-xs font-semibold uppercase tracking-wide" :class="col.headerClass">{{ col.label }}</span>
|
|
|
|
|
|
<span class="text-xs text-gray-600">({{ kanbanTasksByStatus[col.status]?.length || 0 }})</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- Drop zone -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="flex flex-col gap-1.5 min-h-24 rounded p-1.5 border transition-colors"
|
|
|
|
|
|
:class="[col.bgClass, dragOverStatus === col.status ? 'border-blue-600' : 'border-transparent']"
|
|
|
|
|
|
@dragover.prevent
|
|
|
|
|
|
@dragenter.prevent="dragOverStatus = col.status"
|
|
|
|
|
|
@dragleave="onDragLeave"
|
|
|
|
|
|
@drop.prevent="onKanbanDrop($event, col.status)">
|
|
|
|
|
|
<router-link
|
|
|
|
|
|
v-for="t in kanbanTasksByStatus[col.status]" :key="t.id"
|
|
|
|
|
|
:to="`/task/${t.id}`"
|
|
|
|
|
|
draggable="true"
|
|
|
|
|
|
@dragstart="onDragStart($event, t.id)"
|
|
|
|
|
|
@dragend="onDragEnd"
|
|
|
|
|
|
class="block px-2.5 py-2 bg-gray-900 border border-gray-800 rounded text-xs hover:border-gray-600 no-underline cursor-grab active:cursor-grabbing transition-colors select-none"
|
|
|
|
|
|
:class="draggingTaskId === t.id ? 'opacity-40' : ''">
|
|
|
|
|
|
<div class="text-gray-500 mb-1 text-[10px]">{{ t.id }}</div>
|
2026-03-17 16:14:35 +02:00
|
|
|
|
<div class="text-gray-300 leading-snug mb-1">{{ t.title }}</div>
|
|
|
|
|
|
<div v-if="t.status === 'blocked' && t.blocked_reason" class="text-xs text-red-400 truncate mb-1">{{ t.blocked_reason }}</div>
|
2026-03-16 10:04:01 +02:00
|
|
|
|
<div class="flex items-center gap-1 flex-wrap">
|
|
|
|
|
|
<Badge v-if="t.category" :text="t.category" :color="CATEGORY_COLORS[t.category] || 'gray'" />
|
|
|
|
|
|
<span class="text-gray-600 text-[10px] ml-auto">p{{ t.priority }}</span>
|
|
|
|
|
|
<span v-if="t.status === 'in_progress'" class="inline-block w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</router-link>
|
|
|
|
|
|
<div v-if="!kanbanTasksByStatus[col.status]?.length" class="text-xs text-gray-700 text-center py-6">—</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-16 10:28:06 +02:00
|
|
|
|
</div>
|
2026-03-16 10:04:01 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-16 19:26:51 +02:00
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
2026-03-17 17:53:14 +02:00
|
|
|
|
<!-- Links Tab -->
|
|
|
|
|
|
<div v-if="activeTab === 'links'">
|
|
|
|
|
|
<div class="flex items-center justify-between mb-3">
|
|
|
|
|
|
<span class="text-xs text-gray-500">Связи между проектами</span>
|
|
|
|
|
|
<button @click="showAddLink = true"
|
|
|
|
|
|
class="px-3 py-1 text-xs bg-gray-800 text-gray-300 border border-gray-700 rounded hover:bg-gray-700">
|
|
|
|
|
|
+ Add Link
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-if="linksLoading" class="text-gray-500 text-sm">Загрузка...</p>
|
|
|
|
|
|
<p v-else-if="linksError" class="text-red-400 text-sm">{{ linksError }}</p>
|
|
|
|
|
|
<div v-else-if="links.length === 0" class="text-gray-600 text-sm">Нет связей. Добавьте зависимости между проектами.</div>
|
|
|
|
|
|
<div v-else class="space-y-2">
|
|
|
|
|
|
<div v-for="link in links" :key="link.id"
|
|
|
|
|
|
class="px-4 py-3 border border-gray-800 rounded hover:border-gray-700 flex items-center justify-between gap-3">
|
|
|
|
|
|
<div class="flex items-center gap-2 text-sm flex-wrap">
|
|
|
|
|
|
<span class="text-gray-400 font-mono text-xs">{{ link.from_project }}</span>
|
|
|
|
|
|
<span class="text-gray-600">-></span>
|
|
|
|
|
|
<span class="text-gray-400 font-mono text-xs">{{ link.to_project }}</span>
|
2026-03-17 18:41:21 +02:00
|
|
|
|
<span class="px-1.5 py-0.5 text-[10px] bg-indigo-900/30 text-indigo-400 border border-indigo-800 rounded">{{ link.type }}</span>
|
2026-03-17 17:53:14 +02:00
|
|
|
|
<span v-if="link.description" class="text-gray-500 text-xs">{{ link.description }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button @click="deleteLink(link.id)"
|
|
|
|
|
|
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 shrink-0">
|
|
|
|
|
|
x
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Add Link Modal -->
|
2026-03-17 18:41:21 +02:00
|
|
|
|
<Modal v-if="showAddLink" title="Add Link" @close="showAddLink = false; linkForm = { to_project: '', type: 'depends_on', description: '' }; linkFormError = ''">
|
2026-03-17 17:53:14 +02:00
|
|
|
|
<form @submit.prevent="addLink" class="space-y-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">From (current project)</label>
|
|
|
|
|
|
<input :value="props.id" disabled
|
|
|
|
|
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-500 font-mono" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">To project</label>
|
|
|
|
|
|
<select v-model="linkForm.to_project" required
|
|
|
|
|
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300">
|
|
|
|
|
|
<option value="">— выберите проект —</option>
|
|
|
|
|
|
<option v-for="p in allProjects.filter(p => p.id !== props.id)" :key="p.id" :value="p.id">{{ p.id }} — {{ p.name }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">Link type</label>
|
2026-03-17 18:41:21 +02:00
|
|
|
|
<select v-model="linkForm.type"
|
2026-03-17 17:53:14 +02:00
|
|
|
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300">
|
|
|
|
|
|
<option value="depends_on">depends_on</option>
|
|
|
|
|
|
<option value="triggers">triggers</option>
|
|
|
|
|
|
<option value="related_to">related_to</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">Description (optional)</label>
|
|
|
|
|
|
<input v-model="linkForm.description" placeholder="e.g. API used by frontend"
|
|
|
|
|
|
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 v-if="linkFormError" class="text-red-400 text-xs">{{ linkFormError }}</p>
|
|
|
|
|
|
<div class="flex gap-2 justify-end">
|
|
|
|
|
|
<button type="button" @click="showAddLink = false; linkFormError = ''"
|
2026-03-18 07:57:15 +02:00
|
|
|
|
class="px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200">{{ t('common.cancel') }}</button>
|
2026-03-17 17:53:14 +02:00
|
|
|
|
<button type="submit" :disabled="linkSaving"
|
|
|
|
|
|
class="px-4 py-1.5 text-sm bg-blue-900/50 text-blue-400 border border-blue-800 rounded hover:bg-blue-900 disabled:opacity-50">
|
|
|
|
|
|
{{ linkSaving ? 'Saving...' : 'Add Link' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-18 14:30:36 +02:00
|
|
|
|
<!-- Settings Tab -->
|
|
|
|
|
|
<div v-if="activeTab === 'settings'" class="space-y-6 max-w-2xl">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Agent Execution Section -->
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3">{{ t('projectView.settings_agent_section') }}</p>
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('projectView.settings_execution_mode') }}</label>
|
|
|
|
|
|
<select v-model="settingsForm.execution_mode"
|
|
|
|
|
|
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300 focus:outline-none focus:border-gray-500">
|
|
|
|
|
|
<option value="review">review</option>
|
|
|
|
|
|
<option value="auto_complete">auto_complete</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer select-none">
|
|
|
|
|
|
<input type="checkbox" v-model="settingsForm.autocommit_enabled" class="w-4 h-4 rounded border-gray-600 bg-gray-800 accent-blue-500 cursor-pointer" />
|
|
|
|
|
|
<span class="text-sm text-gray-300">{{ t('projectView.settings_autocommit') }}</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500">{{ t('projectView.settings_autocommit_hint') }}</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer select-none">
|
|
|
|
|
|
<input type="checkbox" v-model="settingsForm.auto_test_enabled" class="w-4 h-4 rounded border-gray-600 bg-gray-800 accent-blue-500 cursor-pointer" />
|
|
|
|
|
|
<span class="text-sm text-gray-300">{{ t('settings.auto_test') }}</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500">{{ t('settings.auto_test_hint') }}</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer select-none">
|
|
|
|
|
|
<input type="checkbox" v-model="settingsForm.worktrees_enabled" class="w-4 h-4 rounded border-gray-600 bg-gray-800 accent-blue-500 cursor-pointer" />
|
|
|
|
|
|
<span class="text-sm text-gray-300">{{ t('settings.worktrees') }}</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500">{{ t('settings.worktrees_hint') }}</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.test_command') }}</label>
|
|
|
|
|
|
<input v-model="settingsForm.test_command" type="text" placeholder="make test"
|
|
|
|
|
|
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500" />
|
|
|
|
|
|
<p class="text-xs text-gray-600 mt-1">{{ t('settings.test_command_hint') }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Deploy Section -->
|
|
|
|
|
|
<div class="pt-4 border-t border-gray-800">
|
|
|
|
|
|
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3">{{ t('projectView.settings_deploy_section') }}</p>
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.server_host') }}</label>
|
|
|
|
|
|
<input v-model="settingsForm.deploy_host" type="text" placeholder="vdp-prod"
|
|
|
|
|
|
class="w-full bg-gray-900 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>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.project_path_on_server') }}</label>
|
|
|
|
|
|
<input v-model="settingsForm.deploy_path" type="text" placeholder="/srv/myproject"
|
|
|
|
|
|
class="w-full bg-gray-900 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>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.runtime') }}</label>
|
|
|
|
|
|
<select v-model="settingsForm.deploy_runtime"
|
|
|
|
|
|
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300 focus:outline-none focus:border-gray-500">
|
|
|
|
|
|
<option value="">{{ t('settings.select_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>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.restart_command') }}</label>
|
|
|
|
|
|
<input v-model="settingsForm.deploy_restart_cmd" type="text" placeholder="optional override command"
|
|
|
|
|
|
class="w-full bg-gray-900 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>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.fallback_command') }}</label>
|
|
|
|
|
|
<input v-model="settingsForm.deploy_command" type="text" placeholder="git push origin main"
|
|
|
|
|
|
class="w-full bg-gray-900 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>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Integrations Section -->
|
|
|
|
|
|
<div class="pt-4 border-t border-gray-800">
|
|
|
|
|
|
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3">{{ t('projectView.settings_integrations_section') }}</p>
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.obsidian_vault_path') }}</label>
|
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
|
<input v-model="settingsForm.obsidian_vault_path" type="text" placeholder="/path/to/obsidian/vault"
|
|
|
|
|
|
class="flex-1 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500" />
|
|
|
|
|
|
<button @click="syncObsidianVault" :disabled="syncingObsidian || !settingsForm.obsidian_vault_path"
|
|
|
|
|
|
class="px-3 py-1.5 text-sm bg-indigo-900/50 text-indigo-400 border border-indigo-800 rounded hover:bg-indigo-900 disabled:opacity-50">
|
|
|
|
|
|
{{ syncingObsidian ? t('settings.syncing') : t('settings.sync_obsidian') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="obsidianSyncResult" class="mt-2 p-2 bg-gray-900 rounded text-xs text-gray-300 flex gap-3">
|
|
|
|
|
|
<span>{{ obsidianSyncResult.exported_decisions }} decisions</span>
|
|
|
|
|
|
<span>{{ obsidianSyncResult.tasks_updated }} tasks</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-03-18 15:28:20 +02:00
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.ssh_host') }}</label>
|
2026-03-18 14:30:36 +02:00
|
|
|
|
<input v-model="settingsForm.ssh_host" type="text" placeholder="vdp-prod"
|
|
|
|
|
|
class="w-full bg-gray-900 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>
|
2026-03-18 15:28:20 +02:00
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.ssh_user') }}</label>
|
2026-03-18 14:30:36 +02:00
|
|
|
|
<input v-model="settingsForm.ssh_user" type="text" placeholder="root"
|
|
|
|
|
|
class="w-full bg-gray-900 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>
|
2026-03-18 15:28:20 +02:00
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.ssh_key_path') }}</label>
|
2026-03-18 14:30:36 +02:00
|
|
|
|
<input v-model="settingsForm.ssh_key_path" type="text" placeholder="~/.ssh/id_rsa"
|
|
|
|
|
|
class="w-full bg-gray-900 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>
|
2026-03-18 15:28:20 +02:00
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">{{ t('settings.ssh_proxy_jump') }}</label>
|
2026-03-18 14:30:36 +02:00
|
|
|
|
<input v-model="settingsForm.ssh_proxy_jump" type="text" placeholder="jumpt"
|
|
|
|
|
|
class="w-full bg-gray-900 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>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Save Button -->
|
|
|
|
|
|
<div class="flex items-center gap-3 pt-4 border-t border-gray-800">
|
|
|
|
|
|
<button @click="saveSettings" :disabled="settingsSaving"
|
|
|
|
|
|
class="px-4 py-2 text-sm bg-blue-900/50 text-blue-400 border border-blue-800 rounded hover:bg-blue-900 disabled:opacity-50">
|
|
|
|
|
|
{{ settingsSaving ? t('common.saving') : t('common.save') }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span v-if="settingsSaveStatus" class="text-xs"
|
|
|
|
|
|
:class="settingsSaveStatus.startsWith(t('common.error')) ? 'text-red-400' : 'text-green-400'">
|
|
|
|
|
|
{{ settingsSaveStatus }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<!-- Add Task Modal -->
|
2026-03-16 22:35:31 +02:00
|
|
|
|
<Modal v-if="showAddTask" title="Add Task" @close="closeAddTaskModal">
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<form @submit.prevent="addTask" class="space-y-3">
|
|
|
|
|
|
<input v-model="taskForm.title" placeholder="Task title" required
|
|
|
|
|
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
|
2026-03-16 08:34:30 +02:00
|
|
|
|
<select v-model="taskForm.category"
|
|
|
|
|
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300">
|
|
|
|
|
|
<option value="">No category (old format: PROJ-001)</option>
|
|
|
|
|
|
<option v-for="cat in TASK_CATEGORIES" :key="cat" :value="cat">{{ cat }}</option>
|
|
|
|
|
|
</select>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<select v-model="taskForm.route_type"
|
|
|
|
|
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300">
|
|
|
|
|
|
<option value="">No type</option>
|
|
|
|
|
|
<option value="debug">debug</option>
|
|
|
|
|
|
<option value="feature">feature</option>
|
|
|
|
|
|
<option value="refactor">refactor</option>
|
|
|
|
|
|
<option value="hotfix">hotfix</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<input v-model.number="taskForm.priority" type="number" min="1" max="10" placeholder="Priority"
|
|
|
|
|
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
|
2026-03-16 10:04:01 +02:00
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">Критерии приёмки</label>
|
|
|
|
|
|
<textarea v-model="taskForm.acceptance_criteria" rows="3"
|
|
|
|
|
|
placeholder="Что должно быть на выходе? Какой результат считается успешным?"
|
|
|
|
|
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 resize-y"></textarea>
|
|
|
|
|
|
</div>
|
2026-03-16 22:35:31 +02:00
|
|
|
|
<div v-if="project?.path">
|
|
|
|
|
|
<label class="block text-xs text-gray-500 mb-1">Вложения</label>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<button type="button" @click="fileInputRef?.click()"
|
|
|
|
|
|
class="px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-xs text-gray-400 hover:bg-gray-700 hover:text-gray-200">
|
|
|
|
|
|
Прикрепить файлы
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span v-if="pendingFiles.length" class="text-xs text-gray-500">{{ pendingFiles.length }} файл(ов)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<input ref="fileInputRef" type="file" multiple class="hidden" @change="onFileSelect" />
|
|
|
|
|
|
<ul v-if="pendingFiles.length" class="mt-2 space-y-1">
|
|
|
|
|
|
<li v-for="(file, i) in pendingFiles" :key="i"
|
|
|
|
|
|
class="flex items-center justify-between text-xs text-gray-400 bg-gray-800/50 rounded px-2 py-1">
|
|
|
|
|
|
<span class="truncate">{{ file.name }}</span>
|
|
|
|
|
|
<button type="button" @click="pendingFiles.splice(i, 1)"
|
|
|
|
|
|
class="ml-2 text-gray-600 hover:text-red-400 flex-shrink-0">✕</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
<p v-if="taskFormError" class="text-red-400 text-xs">{{ taskFormError }}</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
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Add Decision Modal -->
|
|
|
|
|
|
<Modal v-if="showAddDecision" title="Add Decision" @close="showAddDecision = false">
|
|
|
|
|
|
<form @submit.prevent="addDecision" class="space-y-3">
|
|
|
|
|
|
<select v-model="decForm.type" required
|
|
|
|
|
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300">
|
|
|
|
|
|
<option value="decision">decision</option>
|
|
|
|
|
|
<option value="gotcha">gotcha</option>
|
|
|
|
|
|
<option value="workaround">workaround</option>
|
|
|
|
|
|
<option value="convention">convention</option>
|
|
|
|
|
|
<option value="rejected_approach">rejected_approach</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<input v-model="decForm.title" placeholder="Title" required
|
|
|
|
|
|
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="decForm.description" placeholder="Description" rows="3" required
|
|
|
|
|
|
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 resize-y"></textarea>
|
|
|
|
|
|
<input v-model="decForm.category" placeholder="Category (e.g. ui, api, security)"
|
|
|
|
|
|
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="decForm.tags" placeholder="Tags (comma-separated)"
|
|
|
|
|
|
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="decFormError" class="text-red-400 text-xs">{{ decFormError }}</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
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</Modal>
|
2026-03-15 17:44:16 +02:00
|
|
|
|
|
2026-03-16 19:26:51 +02:00
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
2026-03-16 22:35:31 +02:00
|
|
|
|
<!-- Task Revise Modal -->
|
|
|
|
|
|
<Modal v-if="showTaskReviseModal" title="Отправить на доработку" @close="showTaskReviseModal = false">
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
<p class="text-xs text-gray-500">Задача <span class="text-orange-400">{{ taskReviseTaskId }}</span> вернётся в pipeline с вашим комментарием.</p>
|
|
|
|
|
|
<textarea v-model="taskReviseComment" placeholder="Что нужно доработать?" 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>
|
|
|
|
|
|
<p v-if="taskReviseError" class="text-red-400 text-xs">{{ taskReviseError }}</p>
|
|
|
|
|
|
<button @click="submitTaskRevise" :disabled="taskReviseSaving"
|
|
|
|
|
|
class="w-full py-2 bg-orange-900/50 text-orange-400 border border-orange-800 rounded text-sm hover:bg-orange-900 disabled:opacity-50">
|
2026-03-18 07:57:15 +02:00
|
|
|
|
{{ taskReviseSaving ? t('common.saving') : t('taskDetail.send_to_revision') }}
|
2026-03-16 22:35:31 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
2026-03-15 17:44:16 +02:00
|
|
|
|
<!-- 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">
|
|
|
|
|
|
Audit failed: {{ auditResult.error }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="space-y-4">
|
|
|
|
|
|
<div v-if="auditResult.already_done?.length">
|
|
|
|
|
|
<h3 class="text-sm font-semibold text-green-400 mb-2">Already done ({{ auditResult.already_done.length }})</h3>
|
|
|
|
|
|
<div v-for="item in auditResult.already_done" :key="item.id"
|
|
|
|
|
|
class="px-3 py-2 border border-green-900/50 rounded text-xs mb-1">
|
|
|
|
|
|
<span class="text-green-400 font-medium">{{ item.id }}</span>
|
|
|
|
|
|
<span class="text-gray-400 ml-2">{{ item.reason }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="auditResult.still_pending?.length">
|
|
|
|
|
|
<h3 class="text-sm font-semibold text-gray-400 mb-2">Still pending ({{ auditResult.still_pending.length }})</h3>
|
|
|
|
|
|
<div v-for="item in auditResult.still_pending" :key="item.id"
|
|
|
|
|
|
class="px-3 py-2 border border-gray-800 rounded text-xs mb-1">
|
|
|
|
|
|
<span class="text-gray-300 font-medium">{{ item.id }}</span>
|
|
|
|
|
|
<span class="text-gray-500 ml-2">{{ item.reason }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="auditResult.unclear?.length">
|
|
|
|
|
|
<h3 class="text-sm font-semibold text-yellow-400 mb-2">Unclear ({{ auditResult.unclear.length }})</h3>
|
|
|
|
|
|
<div v-for="item in auditResult.unclear" :key="item.id"
|
|
|
|
|
|
class="px-3 py-2 border border-yellow-900/50 rounded text-xs mb-1">
|
|
|
|
|
|
<span class="text-yellow-400 font-medium">{{ item.id }}</span>
|
|
|
|
|
|
<span class="text-gray-400 ml-2">{{ item.reason }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="auditResult.cost_usd || auditResult.duration_seconds" class="text-xs text-gray-600">
|
|
|
|
|
|
<span v-if="auditResult.duration_seconds">{{ auditResult.duration_seconds }}s</span>
|
|
|
|
|
|
<span v-if="auditResult.cost_usd" class="ml-2">${{ auditResult.cost_usd?.toFixed(4) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button v-if="auditResult.already_done?.length" @click="applyAudit" :disabled="auditApplying"
|
|
|
|
|
|
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">
|
|
|
|
|
|
{{ auditApplying ? 'Applying...' : `Mark ${auditResult.already_done.length} tasks as done` }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
Add web GUI: FastAPI API + Vue 3 frontend with dark theme
API (web/api.py):
GET /api/projects, /api/projects/{id}, /api/tasks/{id}
GET /api/decisions?project=X, /api/cost?days=7, /api/support/tickets
POST /api/projects, /api/tasks, /api/decisions, /api/bootstrap
CORS for localhost:5173, all queries via models.py
Frontend (web/frontend/):
Vue 3 + TypeScript + Vite + Tailwind CSS v3
Dashboard: project cards with task counters, cost, status badges
ProjectView: tabs for Tasks/Decisions/Modules with filters
Modals: Add Project, Add Task, Add Decision, Bootstrap
Dark theme, monospace font, minimal clean design
Startup:
API: cd web && uvicorn api:app --reload --port 8420
Web: cd web/frontend && npm install && npm run dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:50:15 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|