feat: découverte automatique des dépôts git (scan + montrer/cacher)
Scan borné du système de fichiers (racines configurables, défaut home ; profondeur/nombre/timeout bornés ; symlinks non suivis ; exclusions node_modules/dotdirs) qui auto-enregistre les nouveaux dépôts. Insertion atomique ON CONFLICT DO NOTHING (idempotence + anti-résurrection d'un dépôt masqué + anti-course). Scan au démarrage + bouton manuel + re-scan périodique (RepoDiscoveryService, démarré dans runDaemon). Colonne repos.hidden : masquer = conservé en DB mais exclu du dashboard et jamais ré-ajouté ; supprimer = re-découvrable. UI : bouton œil par dépôt + bascule afficher-les-masqués sur le dashboard, section Découverte dans les Réglages (racines + intervalle, allow-list stricte). Robustesse : listAllWorktrees tolère l'échec git par dépôt ; flag --no-discover (escape hatch + hermétisme des acceptations).
This commit is contained in:
@@ -121,6 +121,41 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Découverte des dépôts -->
|
||||
<section class="card flex flex-col gap-3">
|
||||
<h2 class="flex items-center gap-2 text-sm font-semibold text-zinc-100">
|
||||
<ScanSearch :size="16" /> {{ t('settings.discovery') }}
|
||||
</h2>
|
||||
<p class="text-xs text-zinc-500">{{ t('settings.discoveryHint') }}</p>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs text-zinc-400">{{ t('settings.scanRoots') }}</span>
|
||||
<p v-if="rootsDraft.length === 0" class="text-xs text-zinc-600">{{ t('settings.scanRootsEmpty') }}</p>
|
||||
<ul v-else class="flex flex-col gap-1">
|
||||
<li v-for="(root, i) in rootsDraft" :key="root" class="card-inset flex items-center gap-2">
|
||||
<span class="min-w-0 flex-1 truncate font-mono text-xs text-zinc-200" :title="root">{{ root }}</span>
|
||||
<BaseButton size="sm" variant="ghost" icon-only :icon="X" :aria-label="t('settings.removeRoot')" @click="rootsDraft.splice(i, 1)" />
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<BaseButton size="sm" :icon="FolderOpen" @click="showRootPicker = !showRootPicker">{{ t('settings.addRoot') }}</BaseButton>
|
||||
</div>
|
||||
<DirectoryPicker v-if="showRootPicker" mode="dir" @select="onAddRoot" @close="showRootPicker = false" />
|
||||
</div>
|
||||
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs text-zinc-400">{{ t('settings.scanInterval') }}</span>
|
||||
<input v-model.number="intervalDraft" type="number" min="0" max="1440" class="input w-32" />
|
||||
<span class="text-xs text-zinc-500">{{ t('settings.scanIntervalHint') }}</span>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<BaseButton variant="primary" :loading="settings.saving" :disabled="!discoveryDirty" @click="saveDiscovery">
|
||||
{{ t('settings.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Serveur (lecture seule) -->
|
||||
<section class="card flex flex-col gap-3">
|
||||
<h2 class="flex items-center gap-2 text-sm font-semibold text-zinc-100">
|
||||
@@ -148,7 +183,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Bell, BellOff, Check, Copy, KeyRound, Plus, Puzzle, Send, Server, Shield, SlidersHorizontal, Trash2, X } from '@lucide/vue';
|
||||
import { Bell, BellOff, Check, Copy, FolderOpen, KeyRound, Plus, Puzzle, ScanSearch, Send, Server, Shield, SlidersHorizontal, Trash2, X } from '@lucide/vue';
|
||||
import type { CreateTokenResponse, TokenInfo, TokensListResponse } from '@arboretum/shared';
|
||||
import { api, ApiError } from '../lib/api';
|
||||
import { useSettingsStore } from '../stores/settings';
|
||||
@@ -159,6 +194,7 @@ import LanguageSwitcher from '../components/LanguageSwitcher.vue';
|
||||
import BaseButton from '../components/ui/BaseButton.vue';
|
||||
import SkeletonRow from '../components/ui/SkeletonRow.vue';
|
||||
import ServerRow from '../components/settings/ServerRow.vue';
|
||||
import DirectoryPicker from '../components/DirectoryPicker.vue';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const settings = useSettingsStore();
|
||||
@@ -249,9 +285,38 @@ async function saveGitea(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Découverte des dépôts ----
|
||||
const rootsDraft = ref<string[]>([]);
|
||||
const intervalDraft = ref(0);
|
||||
const showRootPicker = ref(false);
|
||||
const discoveryDirty = computed(
|
||||
() =>
|
||||
intervalDraft.value !== settings.scanIntervalMin ||
|
||||
rootsDraft.value.length !== settings.scanRoots.length ||
|
||||
rootsDraft.value.some((r, i) => r !== settings.scanRoots[i]),
|
||||
);
|
||||
function syncDiscoveryDraft(): void {
|
||||
rootsDraft.value = [...settings.scanRoots];
|
||||
intervalDraft.value = settings.scanIntervalMin;
|
||||
}
|
||||
function onAddRoot(path: string): void {
|
||||
if (!rootsDraft.value.includes(path)) rootsDraft.value.push(path);
|
||||
showRootPicker.value = false;
|
||||
}
|
||||
async function saveDiscovery(): Promise<void> {
|
||||
try {
|
||||
await settings.save({ scanRoots: rootsDraft.value, scanIntervalMin: intervalDraft.value });
|
||||
syncDiscoveryDraft();
|
||||
toasts.success(t('settings.saved'));
|
||||
} catch (e) {
|
||||
toasts.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadTokens(), settings.loaded ? Promise.resolve() : settings.fetch()]);
|
||||
giteaInput.value = settings.giteaUrl ?? '';
|
||||
syncDiscoveryDraft();
|
||||
void push.refresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user