feat: onglets Réglages & Aide + icône Gitea

Réglages : préférences (langue, notifications push + test), gestion complète des tokens d'accès (liste/création/révocation, garde anti lock-out sur le dernier token), URL Gitea configurable, infos serveur en lecture seule (port/bind/origines/VAPID + flags CLI). Aide : documentation bilingue EN/FR de toutes les fonctionnalités, avec recherche. Icône Gitea (lien externe) dans la nav (sidebar + MoreSheet mobile).

Backend : routes /api/v1/auth/tokens (GET/POST/DELETE) + tokenId dans /me ; routes/settings.ts (GET/PATCH, allow-list stricte gitea_url, aucun secret exposé, URL validée http/https anti-XSS) ; AuthService.listTokens/revokeToken (transaction). Front : NavItem gère les liens externes, nav primaire/secondaire, store settings, vues SettingsView/HelpView.

236 tests verts (+15 nouveaux : auth-tokens, settings-routes).
This commit is contained in:
2026-06-18 14:16:53 +02:00
parent 5c1c2a8591
commit c9670ca309
23 changed files with 1469 additions and 24 deletions

View File

@@ -1,12 +1,17 @@
import { computed, type Component } from 'vue';
import { useRoute, type RouteLocationRaw } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { GitBranch, TerminalSquare, Boxes } from '@lucide/vue';
import { Boxes, GitBranch, LifeBuoy, Settings, TerminalSquare } from '@lucide/vue';
import { useSessionsStore } from '../stores/sessions';
import { useSettingsStore } from '../stores/settings';
import GiteaIcon from '../components/ui/GiteaIcon.vue';
export interface NavEntry {
key: string;
to: RouteLocationRaw;
/** route interne (exclusif avec `href`). */
to?: RouteLocationRaw;
/** lien externe ouvert dans un nouvel onglet (exclusif avec `to`). */
href?: string;
icon: Component;
label: string;
match: string[]; // noms de routes considérés actifs pour cet item
@@ -18,19 +23,33 @@ export function useNav() {
const { t } = useI18n();
const route = useRoute();
const sessions = useSessionsStore();
const settings = useSettingsStore();
// sessions vivantes bloquées sur un dialogue (le cœur de la supervision mobile).
const waitingCount = computed(
() => sessions.sessions.filter((s) => s.live && s.activity === 'waiting').length,
);
const items = computed<NavEntry[]>(() => [
// Onglets principaux : barre du bas mobile + haut de sidebar.
const primary = computed<NavEntry[]>(() => [
{ key: 'worktrees', to: { name: 'dashboard' }, icon: GitBranch, label: t('nav.worktrees'), match: ['dashboard'], badge: 0 },
{ key: 'sessions', to: { name: 'sessions' }, icon: TerminalSquare, label: t('nav.sessions'), match: ['sessions', 'session'], badge: waitingCount.value },
{ key: 'groups', to: { name: 'groups' }, icon: Boxes, label: t('nav.groups'), match: ['groups', 'group'], badge: 0 },
]);
// Onglets secondaires : réglages, aide, et lien externe Gitea (uniquement si l'URL est configurée).
const secondary = computed<NavEntry[]>(() => {
const items: NavEntry[] = [
{ key: 'settings', to: { name: 'settings' }, icon: Settings, label: t('nav.settings'), match: ['settings'], badge: 0 },
{ key: 'help', to: { name: 'help' }, icon: LifeBuoy, label: t('nav.help'), match: ['help'], badge: 0 },
];
if (settings.giteaUrl) {
items.push({ key: 'gitea', href: settings.giteaUrl, icon: GiteaIcon, label: t('nav.gitea'), match: [], badge: 0 });
}
return items;
});
const isActive = (match: string[]): boolean => match.includes(String(route.name));
return { items, isActive, waitingCount };
return { primary, secondary, isActive, waitingCount };
}