P3-C: dashboard worktree-first + acceptance P3 (MVP complet)
Vue racine consolidée : repos → worktrees avec état git ET état de session corrélé. Complète le MVP (P2 découverte/reprise + P3-A worktrees + P3-B états fins). - web/lib/ws-client: abonnement multi-topics (sessions + worktrees), subscribeWorktrees. - web/stores/worktrees: repos + worktrees, CRUD, temps réel (repo_update/worktree_*). - web/views/DashboardView (route racine /), components RepoSection + WorktreeCard ; SessionStateBadge réutilisé pour l'état des sessions corrélées. - router: / = dashboard, /sessions = liste à plat (sessions hors worktree), nav croisée. - i18n EN/FR (dashboard/repos/worktrees). - fix: @xterm/headless est CommonJS → chargé via createRequire (l'import nommé ESM échouait sous Node natif, masqué par esbuild en test) ; détecté par l'acceptation. - scripts/acceptance-p3.mjs: repo git tmp → enregistrement, worktree + hook, broadcast WS worktree_update, corrélation session bash, suppression (409 sans force, 200 avec). Vérifs : typecheck, 159 tests, build (vue-tsc), acceptations P1/P2/P3 ALL GREEN.
This commit is contained in:
82
packages/web/src/views/DashboardView.vue
Normal file
82
packages/web/src/views/DashboardView.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="overflow-y-auto">
|
||||
<div class="mx-auto flex max-w-3xl flex-col gap-4 px-4 py-6">
|
||||
<header class="flex items-center gap-3">
|
||||
<h1 class="text-lg font-semibold text-zinc-100">{{ t('dashboard.title') }}</h1>
|
||||
<span v-if="auth.serverVersion" class="text-xs text-zinc-600">v{{ auth.serverVersion }}</span>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<RouterLink :to="{ name: 'sessions' }" class="btn">{{ t('dashboard.allSessions') }}</RouterLink>
|
||||
<button class="btn" :disabled="store.loading" @click="store.fetchAll()">{{ t('sessions.refresh') }}</button>
|
||||
<LanguageSwitcher />
|
||||
<button class="btn" @click="onLogout">{{ t('common.logout') }}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form class="flex flex-col gap-2 rounded-lg border border-zinc-800 bg-zinc-900/50 p-3 sm:flex-row sm:items-end" @submit.prevent="onAddRepo">
|
||||
<label class="flex flex-1 flex-col gap-1 text-xs text-zinc-400">
|
||||
{{ t('repos.pathLabel') }}
|
||||
<input v-model="newPath" type="text" class="input font-mono" :placeholder="t('repos.pathPlaceholder')" required />
|
||||
</label>
|
||||
<button type="submit" class="btn-primary whitespace-nowrap" :disabled="adding || newPath.trim() === ''">
|
||||
{{ adding ? t('repos.adding') : t('repos.add') }}
|
||||
</button>
|
||||
</form>
|
||||
<p v-if="addError" class="text-sm text-red-400">{{ addError }}</p>
|
||||
|
||||
<p v-if="store.loadError" class="text-sm text-red-400">{{ store.loadError }}</p>
|
||||
<p v-else-if="store.loading && store.repos.length === 0" class="text-sm text-zinc-500">{{ t('common.loading') }}</p>
|
||||
<p v-else-if="store.repos.length === 0" class="text-sm text-zinc-500">{{ t('repos.empty') }}</p>
|
||||
|
||||
<RepoSection v-for="repo in store.repos" :key="repo.id" :repo="repo" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { useWorktreesStore } from '../stores/worktrees';
|
||||
import { useSessionsStore } from '../stores/sessions';
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher.vue';
|
||||
import RepoSection from '../components/RepoSection.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const store = useWorktreesStore();
|
||||
const sessions = useSessionsStore();
|
||||
|
||||
const newPath = ref('');
|
||||
const adding = ref(false);
|
||||
const addError = ref<string | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
void store.fetchAll();
|
||||
store.startRealtime();
|
||||
// les sessions alimentent la corrélation worktree↔session (cwd) côté serveur ; on suit aussi
|
||||
// leurs mises à jour temps réel pour que les badges d'état se rafraîchissent.
|
||||
sessions.startRealtime();
|
||||
});
|
||||
|
||||
async function onAddRepo(): Promise<void> {
|
||||
adding.value = true;
|
||||
addError.value = null;
|
||||
try {
|
||||
await store.addRepo(newPath.value.trim());
|
||||
newPath.value = '';
|
||||
} catch (err) {
|
||||
addError.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
adding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onLogout(): Promise<void> {
|
||||
store.stopRealtime();
|
||||
sessions.stopRealtime();
|
||||
await auth.logout();
|
||||
await router.replace({ name: 'login' });
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user