kin: KIN-120-frontend_dev
This commit is contained in:
parent
e3a286ef6f
commit
a202210b9f
5 changed files with 333 additions and 560 deletions
|
|
@ -2,7 +2,7 @@
|
|||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { api, ApiError, type ProjectDetail, type AuditResult, type Phase, type Task, type ProjectEnvironment, type DeployResult, type ProjectLink } from '../api'
|
||||
import { api, ApiError, type ProjectDetail, type AuditResult, type Phase, type Task, type ProjectEnvironment, type DeployResult, type ProjectLink, type ObsidianSyncResult } from '../api'
|
||||
import Badge from '../components/Badge.vue'
|
||||
import Modal from '../components/Modal.vue'
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ const { t } = useI18n()
|
|||
const project = ref<ProjectDetail | null>(null)
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments' | 'links'>('tasks')
|
||||
const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments' | 'links' | 'settings'>('tasks')
|
||||
|
||||
// Phases
|
||||
const phases = ref<Phase[]>([])
|
||||
|
|
@ -276,6 +276,99 @@ async function toggleWorktrees() {
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Audit
|
||||
const auditLoading = ref(false)
|
||||
const auditResult = ref<AuditResult | null>(null)
|
||||
|
|
@ -504,6 +597,7 @@ async function load() {
|
|||
loadAutocommit()
|
||||
loadAutoTest()
|
||||
loadWorktrees()
|
||||
loadSettingsForm()
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
|
|
@ -534,6 +628,9 @@ onMounted(async () => {
|
|||
const all = await api.projects()
|
||||
allProjects.value = all.map(p => ({ id: p.id, name: p.name }))
|
||||
} catch {}
|
||||
if (route.query.tab === 'settings') {
|
||||
activeTab.value = 'settings'
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
@ -874,13 +971,13 @@ async function addDecision() {
|
|||
|
||||
<!-- Tabs -->
|
||||
<div class="flex gap-1 mb-4 border-b border-gray-800 flex-wrap">
|
||||
<button v-for="tab in (['tasks', 'phases', 'decisions', 'modules', 'kanban', 'environments', 'links'] as const)" :key="tab"
|
||||
<button v-for="tab in (['tasks', 'phases', 'decisions', 'modules', 'kanban', 'environments', 'links', 'settings'] as const)" :key="tab"
|
||||
@click="activeTab = tab"
|
||||
class="px-4 py-2 text-sm border-b-2 transition-colors"
|
||||
:class="activeTab === tab
|
||||
? 'text-gray-200 border-blue-500'
|
||||
: 'text-gray-500 border-transparent hover:text-gray-300'">
|
||||
{{ tab === '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') : t('projectView.links_tab') }}
|
||||
{{ 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') }}
|
||||
<span class="text-xs text-gray-600 ml-1">
|
||||
{{ tab === 'tasks' ? project.tasks.length
|
||||
: tab === 'phases' ? phases.length
|
||||
|
|
@ -888,7 +985,8 @@ async function addDecision() {
|
|||
: tab === 'modules' ? project.modules.length
|
||||
: tab === 'environments' ? environments.length
|
||||
: tab === 'links' ? links.length
|
||||
: project.tasks.length }}
|
||||
: tab === 'kanban' ? project.tasks.length
|
||||
: '' }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1444,6 +1542,138 @@ async function addDecision() {
|
|||
</Modal>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<label class="block text-xs text-gray-500 mb-1">SSH Host</label>
|
||||
<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>
|
||||
<label class="block text-xs text-gray-500 mb-1">SSH User</label>
|
||||
<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>
|
||||
<label class="block text-xs text-gray-500 mb-1">SSH Key Path</label>
|
||||
<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>
|
||||
<label class="block text-xs text-gray-500 mb-1">SSH ProxyJump</label>
|
||||
<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 Task Modal -->
|
||||
<Modal v-if="showAddTask" title="Add Task" @close="closeAddTaskModal">
|
||||
<form @submit.prevent="addTask" class="space-y-3">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue