diff --git a/.eslintignore b/.eslintignore
index 55d3639e01d9..2ec565fad6f1 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -74,3 +74,4 @@ packages/manager/apps/pci-project
packages/manager/apps/hub
packages/manager/apps/pci-file-storage
packages/manager/apps/network-vrack
+packages/manager/apps/hpc-backup-licenses
diff --git a/.prettierignore b/.prettierignore
index 4708621fb211..b41f0afc4376 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -36,3 +36,4 @@ packages/manager/apps/pci-project
packages/manager/apps/hub
packages/manager/apps/pci-file-storage
packages/manager/apps/network-vrack
+packages/manager/apps/hpc-backup-licenses
diff --git a/.stylelintignore b/.stylelintignore
index b8b98166a193..bbc8fcd4e433 100644
--- a/.stylelintignore
+++ b/.stylelintignore
@@ -18,3 +18,4 @@ packages/manager/apps/zimbra
packages/manager/apps/hub
packages/manager/apps/pci-file-storage
packages/manager/apps/network-vrack
+packages/manager/apps/hpc-backup-licenses
diff --git a/packages/manager-tools/manager-pm/src/playbook/catalog/pnpm-catalog.json b/packages/manager-tools/manager-pm/src/playbook/catalog/pnpm-catalog.json
index 4599e6cabcb2..438320684c17 100644
--- a/packages/manager-tools/manager-pm/src/playbook/catalog/pnpm-catalog.json
+++ b/packages/manager-tools/manager-pm/src/playbook/catalog/pnpm-catalog.json
@@ -68,5 +68,6 @@
"packages/manager/apps/web-domains",
"packages/manager/core/shell-client",
"packages/manager/modules/logs-to-customer",
- "packages/manager/modules/metrics-to-customer"
-]
+ "packages/manager/modules/metrics-to-customer",
+ "packages/manager/apps/hpc-backup-licenses"
+]
\ No newline at end of file
diff --git a/packages/manager/apps/hpc-backup-licenses/README.md b/packages/manager/apps/hpc-backup-licenses/README.md
new file mode 100644
index 000000000000..1cbef84fe8b0
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/README.md
@@ -0,0 +1,19 @@
+# hpc-backup-licenses — OVHcloud Manager Application
+
+## Overview
+
+The hpc-backup-licenses app lets customers choose and link a Veeam licence to a VBR server. Customers can manage multiple VBRs, each with multiple vaults to store their backups.
+
+---
+
+## 📚 Useful Links
+
+- Forge CLI: https://github.com/ovh/manager/tree/master/packages/manager-tools/manager-forge-cli#readme
+- Manager React Shell Client: https://github.com/ovh/manager
+- React Router Docs: https://reactrouter.com/
+- Iceberg API Guide: https://github.com/ovh/manager-core-api
+- OVHcloud Public API Explorer: https://api.ovh.com/
+
+---
+
+**Generated with ❤️ by OVHcloud Forge CLI**
diff --git a/packages/manager/apps/hpc-backup-licenses/eslint.config.mjs b/packages/manager/apps/hpc-backup-licenses/eslint.config.mjs
new file mode 100644
index 000000000000..77c69c2828b1
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/eslint.config.mjs
@@ -0,0 +1,41 @@
+import { a11yEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/a11y';
+import {
+ complexityJsxTsxConfig,
+ complexityTsJsConfig,
+} from '@ovh-ux/manager-static-analysis-kit/eslint/complexity';
+import { htmlEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/html';
+import { javascriptEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/javascript';
+import { prettierEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/prettier';
+import { reactEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/react';
+import { tailwindJsxConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/tailwind-jsx';
+import { tanStackQueryEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/tanstack';
+import { vitestEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/tests';
+import { typescriptEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/typescript';
+
+export default [
+ javascriptEslintConfig,
+ typescriptEslintConfig,
+ reactEslintConfig,
+ a11yEslintConfig,
+ htmlEslintConfig,
+ tailwindJsxConfig,
+ tanStackQueryEslintConfig,
+ vitestEslintConfig,
+ prettierEslintConfig,
+ complexityJsxTsxConfig,
+ complexityTsJsConfig,
+ {
+ files: ['**/__tests__/**/*.{js,jsx,ts,tsx}'],
+ rules: {
+ ...Object.fromEntries(
+ Object.keys({ ...complexityJsxTsxConfig.rules, ...complexityTsJsConfig.rules }).map(
+ (rule) => [rule, 'off'],
+ ),
+ ),
+ 'react/no-multi-comp': 'off',
+ },
+ },
+ {
+ ignores: ['**/*.md', '**/coverage/**', '**/dist/**', '**/*.d.ts'],
+ },
+];
diff --git a/packages/manager/apps/hpc-backup-licenses/index.html b/packages/manager/apps/hpc-backup-licenses/index.html
new file mode 100644
index 000000000000..ec4e92d05007
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+ OVHcloud
+
+
+
+
+
+
+
+
+
diff --git a/packages/manager/apps/hpc-backup-licenses/package.json b/packages/manager/apps/hpc-backup-licenses/package.json
new file mode 100644
index 000000000000..e861cd089b07
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/package.json
@@ -0,0 +1,68 @@
+{
+ "name": "@ovh-ux/manager-hpc-backup-licenses-app",
+ "version": "0.0.0",
+ "private": true,
+ "description": "The hpc-backup-licenses app lets customers choose and link a Veeam licence to a VBR server. Customers can manage multiple VBRs, each with multiple vaults to store their backups.",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ovh/manager.git",
+ "directory": "packages/manager/apps/hpc-backup-licenses"
+ },
+ "license": "BSD-3-Clause",
+ "author": "OVH SAS",
+ "scripts": {
+ "build": "tsc && vite build",
+ "lint": "manager-lint --config eslint.config.mjs ./src",
+ "lint:fix": "manager-lint --fix --config eslint.config.mjs ./src",
+ "start": "vite",
+ "test": "manager-test run",
+ "test:coverage": "manager-test run --coverage"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "5.2.1",
+ "@ovh-ux/manager-common-translations": "*",
+ "@ovh-ux/manager-config": "*",
+ "@ovh-ux/manager-core-api": "*",
+ "@ovh-ux/manager-core-utils": "*",
+ "@ovh-ux/manager-react-components": "2.42.4",
+ "@ovh-ux/manager-react-core-application": "*",
+ "@ovh-ux/manager-react-shell-client": "*",
+ "@ovh-ux/request-tagger": "*",
+ "@ovh-ux/shell": "^4.5.7",
+ "@ovhcloud/ods-components": "~18.6.2",
+ "@ovhcloud/ods-themes": "~18.6.2",
+ "@tanstack/react-query": "5.51.21",
+ "axios": "^1.13.2",
+ "element-internals-polyfill": "^3.0.0",
+ "i18next": "23.8.2",
+ "i18next-http-backend": "2.4.3",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-hook-form": "7.54.2",
+ "react-i18next": "14.1.0",
+ "react-router-dom": "6.16.0",
+ "tailwindcss": "^4.1.17",
+ "zod": "4.1.12"
+ },
+ "devDependencies": {
+ "@ovh-ux/manager-static-analysis-kit": "0.12.0",
+ "@ovh-ux/manager-tailwind-config": "*",
+ "@ovh-ux/manager-tests-setup": "^0.6.1",
+ "@ovh-ux/manager-vite-config": "*",
+ "@tailwindcss/postcss": "^4.1.17",
+ "@tanstack/react-query-devtools": "5.51.21",
+ "@testing-library/react": "^16.0.0",
+ "@types/react": "^18.2.45",
+ "@types/react-dom": "^18.3.0",
+ "msw": "2.12.1",
+ "typescript": "5.9.3",
+ "vite": "7.2.2",
+ "vitest": "3.2.4"
+ },
+ "regions": [
+ "CA",
+ "EU",
+ "US"
+ ],
+ "universes": []
+}
\ No newline at end of file
diff --git a/packages/manager/apps/hpc-backup-licenses/postcss.config.js b/packages/manager/apps/hpc-backup-licenses/postcss.config.js
new file mode 100644
index 000000000000..b03b828ab3b1
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/postcss.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from '@tailwindcss/postcss';
+import autoprefixer from 'autoprefixer';
+
+export default {
+ plugins: [tailwindcss(), autoprefixer()],
+};
diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_de_DE.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_de_DE.json
new file mode 100644
index 000000000000..89722e847929
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_de_DE.json
@@ -0,0 +1,25 @@
+{
+ "title_fallback": "Willkommen bei {{productName}}",
+ "description_part1": "Entdecken Sie, wie Sie {{productName}} optimal nutzen können.",
+ "description_part2": "Befolgen Sie unsere Guides fĂĽr einen schnellen Einstieg.",
+ "guides": {
+ "guide1": {
+ "category": "Entdecken",
+ "title": "Erste Schritte mit {{productName}}",
+ "description": "Lernen Sie die Grundlagen fĂĽr den Einstieg.",
+ "cta": "Jetzt entdecken"
+ },
+ "guide2": {
+ "category": "Tutorials",
+ "title": "Umgebung konfigurieren",
+ "description": "Folgen Sie unserem Guide Schritt fĂĽr Schritt.",
+ "cta": "Zum Tutorial"
+ },
+ "guide3": {
+ "category": "FAQ",
+ "title": "Häufig gestellte Fragen",
+ "description": "Finden Sie Antworten auf häufig gestellte Fragen.",
+ "cta": "FAQs lesen"
+ }
+ }
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_en_GB.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_en_GB.json
new file mode 100644
index 000000000000..b5495cfce41b
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_en_GB.json
@@ -0,0 +1,25 @@
+{
+ "title_fallback": "Welcome to {{productName}}",
+ "description_part1": "Find out how to get the most out of {{productName}}.",
+ "description_part2": "Follow our guides to get started quickly.",
+ "guides": {
+ "guide1": {
+ "category": "Discovery",
+ "title": "Get started with {{productName}}",
+ "description": "Learn the basics to get started.",
+ "cta": "Get started"
+ },
+ "guide2": {
+ "category": "Tutorials",
+ "title": "Configure your environment",
+ "description": "Follow our step-by-step guide.",
+ "cta": "See tutorial"
+ },
+ "guide3": {
+ "category": "FAQs",
+ "title": "Frequently asked questions",
+ "description": "Find answers to frequently asked questions.",
+ "cta": "Read the FAQ"
+ }
+ }
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_es_ES.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_es_ES.json
new file mode 100644
index 000000000000..ad45f9fcc201
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_es_ES.json
@@ -0,0 +1,25 @@
+{
+ "title_fallback": "Bienvenido/a a {{productName}}",
+ "description_part1": "Descubra cómo sacar el máximo partido de {{productName}}.",
+ "description_part2": "Siga nuestras guĂas para empezar rápidamente.",
+ "guides": {
+ "guide1": {
+ "category": "Discovery",
+ "title": "Empezar con {{productName}}",
+ "description": "Descubra los aspectos clave para empezar.",
+ "cta": "Descubrir"
+ },
+ "guide2": {
+ "category": "GuĂas",
+ "title": "Configurar su entorno",
+ "description": "Siga nuestra guĂa paso a paso.",
+ "cta": "Ver el tutorial"
+ },
+ "guide3": {
+ "category": "FAQ",
+ "title": "Preguntas frecuentes",
+ "description": "Encuentre la respuesta a sus preguntas.",
+ "cta": "Leer las FAQ"
+ }
+ }
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_CA.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_CA.json
new file mode 100644
index 000000000000..b0b931acb675
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_CA.json
@@ -0,0 +1,25 @@
+{
+ "title_fallback": "Bienvenue sur {{productName}}",
+ "description_part1": "Découvrez comment tirer le meilleur parti de {{productName}}.",
+ "description_part2": "Suivez nos guides pour démarrer rapidement.",
+ "guides": {
+ "guide1": {
+ "category": "Découverte",
+ "title": "Commencer avec {{productName}}",
+ "description": "Apprenez les bases pour démarrer.",
+ "cta": "Découvrir"
+ },
+ "guide2": {
+ "category": "Tutoriels",
+ "title": "Configurer votre environnement",
+ "description": "Suivez notre guide pas Ă pas.",
+ "cta": "Voir le tutoriel"
+ },
+ "guide3": {
+ "category": "FAQ",
+ "title": "Questions fréquentes",
+ "description": "Trouvez les réponses aux questions courantes.",
+ "cta": "Lire la FAQ"
+ }
+ }
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_FR.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_FR.json
new file mode 100644
index 000000000000..048e6d532c02
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_FR.json
@@ -0,0 +1,25 @@
+{
+ "title_fallback": "Bienvenue sur {{productName}}.",
+ "description_part1": "Découvrez comment tirer le meilleur parti de {{productName}}.",
+ "description_part2": "Suivez nos guides pour démarrer rapidement.",
+ "guides": {
+ "guide1": {
+ "category": "Découverte",
+ "title": "Commencer avec {{productName}}",
+ "description": "Apprenez les bases pour démarrer.",
+ "cta": "Découvrir"
+ },
+ "guide2": {
+ "category": "Tutoriels",
+ "title": "Configurer votre environnement",
+ "description": "Suivez notre guide pas Ă pas.",
+ "cta": "Voir le tutoriel"
+ },
+ "guide3": {
+ "category": "FAQ",
+ "title": "Questions fréquentes",
+ "description": "Trouvez les réponses aux questions courantes.",
+ "cta": "Lire la FAQ"
+ }
+ }
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_it_IT.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_it_IT.json
new file mode 100644
index 000000000000..990c4afbb3a3
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_it_IT.json
@@ -0,0 +1,25 @@
+{
+ "title_fallback": "Benvenuto su {{productName}}",
+ "description_part1": "Scopri come ottenere il massimo da {{productName}}.",
+ "description_part2": "Segui le nostre guide per iniziare rapidamente.",
+ "guides": {
+ "guide1": {
+ "category": "Discovery",
+ "title": "Iniziare a utilizzare {{productName}}",
+ "description": "Concetti base per iniziare.",
+ "cta": "Scopri"
+ },
+ "guide2": {
+ "category": "Tutorial",
+ "title": "Configurare il tuo ambiente",
+ "description": "Segui la nostra guida passo per passo.",
+ "cta": "Guarda il tutorial"
+ },
+ "guide3": {
+ "category": "FAQ",
+ "title": "Domande frequenti",
+ "description": "Trova le risposte alle domande piĂą frequenti.",
+ "cta": "Leggi le FAQ"
+ }
+ }
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pl_PL.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pl_PL.json
new file mode 100644
index 000000000000..05e52b47e7ec
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pl_PL.json
@@ -0,0 +1,25 @@
+{
+ "title_fallback": "Witamy w {{productName}}",
+ "description_part1": "Dowiedz się, jak najlepiej wykorzystać {{productName}}.",
+ "description_part2": "Zapoznaj się z naszymi przewodnikami, aby szybko wystartować.",
+ "guides": {
+ "guide1": {
+ "category": "Pierwsze kroki",
+ "title": "Rozpocznij pracÄ™ z {{productName}}",
+ "description": "Poznaj podstawy i przejdź do działania.",
+ "cta": "SprawdĹş"
+ },
+ "guide2": {
+ "category": "Tutoriale",
+ "title": "Konfiguracja środowiska",
+ "description": "Postępuj zgodnie z instrukcjami z naszego przewodnika",
+ "cta": "SprawdĹş tutorial"
+ },
+ "guide3": {
+ "category": "FAQ",
+ "title": "Najczęściej zadawane pytania",
+ "description": "Znajdź odpowiedzi na często zadawane pytania.",
+ "cta": "Przeczytaj FAQ"
+ }
+ }
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pt_PT.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pt_PT.json
new file mode 100644
index 000000000000..823ef421f8b8
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pt_PT.json
@@ -0,0 +1,25 @@
+{
+ "title_fallback": "Bem-vindo ao {{productName}}",
+ "description_part1": "Saiba como tirar o máximo partido da {{productName}}.",
+ "description_part2": "Siga os nossos guias para começar rapidamente.",
+ "guides": {
+ "guide1": {
+ "category": "Descoberta",
+ "title": "Começar com {{productName}}",
+ "description": "Aprenda o básico para começar.",
+ "cta": "Descobrir"
+ },
+ "guide2": {
+ "category": "Tutoriais",
+ "title": "Configurar o seu ambiente",
+ "description": "Siga o nosso guia passo a passo.",
+ "cta": "Consultar o tutorial"
+ },
+ "guide3": {
+ "category": "FAQ",
+ "title": "Perguntas frequentes",
+ "description": "Encontre as respostas para as perguntas mais habituais.",
+ "cta": "Ler a FAQ"
+ }
+ }
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/src/App.constants.ts b/packages/manager/apps/hpc-backup-licenses/src/App.constants.ts
new file mode 100644
index 000000000000..9fabfa0244d0
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/App.constants.ts
@@ -0,0 +1,22 @@
+import { OnboardingConfigType } from '@/types/Onboarding.type';
+
+export const appName = 'hpc-backup-licenses';
+export const productName = 'Veeam Backup Licenses';
+
+const docUrl = 'https://docs.ovh.com';
+
+export const ONBOARDING_CONFIG: OnboardingConfigType = {
+ productName,
+ productCategory: 'Backup',
+ brand: 'OVHcloud',
+ tiles: [
+ { id: 1, key: 'guide1', linkKey: 'discover' },
+ { id: 2, key: 'guide2', linkKey: 'tutorial' },
+ { id: 3, key: 'guide3', linkKey: 'faq' },
+ ],
+ links: {
+ discover: docUrl,
+ tutorial: docUrl,
+ faq: docUrl,
+ },
+};
diff --git a/packages/manager/apps/hpc-backup-licenses/src/App.tsx b/packages/manager/apps/hpc-backup-licenses/src/App.tsx
new file mode 100644
index 000000000000..4de4224775ba
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/App.tsx
@@ -0,0 +1,44 @@
+import React, { Suspense, useContext, useEffect } from 'react';
+
+import { RouterProvider, createHashRouter, createRoutesFromElements } from 'react-router-dom';
+
+import { QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+
+import { ShellContext } from '@ovh-ux/manager-react-shell-client';
+
+import Routes from '@/routes/Routes';
+
+import queryClient from './QueryClient';
+
+function App() {
+ const { shell } = useContext(ShellContext);
+
+ const router = createHashRouter(createRoutesFromElements(Routes));
+
+ useEffect(() => {
+ const hidePreloader = async () => {
+ try {
+ await shell?.ux?.hidePreloader?.();
+ } catch (err) {
+ console.error('Failed to hide preloader:', err);
+ }
+ };
+
+ void hidePreloader();
+ }, [shell]);
+
+ return (
+
+ {/* Suspense ensures lazy-loaded route components show a fallback UI */}
+ Loading routes ...}>
+
+
+
+ {/* React Query Devtools are included for development and debugging */}
+
+
+ );
+}
+
+export default App;
diff --git a/packages/manager/apps/hpc-backup-licenses/src/QueryClient.ts b/packages/manager/apps/hpc-backup-licenses/src/QueryClient.ts
new file mode 100644
index 000000000000..94100b5328ad
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/QueryClient.ts
@@ -0,0 +1,11 @@
+import { QueryClient } from '@tanstack/react-query';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 300_000,
+ },
+ },
+});
+
+export default queryClient;
diff --git a/packages/manager/apps/hpc-backup-licenses/src/Tracking.constants.ts b/packages/manager/apps/hpc-backup-licenses/src/Tracking.constants.ts
new file mode 100644
index 000000000000..8e2290cdafca
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/Tracking.constants.ts
@@ -0,0 +1,12 @@
+import { appName } from '@/App.constants';
+
+export const LEVEL2 = {
+ EU: { config: { level2: '120' } },
+ CA: { config: { level2: '120' } },
+ US: { config: { level2: '120' } },
+} as const;
+
+export const UNIVERSE = 'HostedPrivatedCloud' as const;
+export const SUB_UNIVERSE = 'HostedPrivatedCloud' as const;
+
+export const APP_NAME = appName;
diff --git a/packages/manager/apps/hpc-backup-licenses/src/assets/error-banner-oops.png b/packages/manager/apps/hpc-backup-licenses/src/assets/error-banner-oops.png
new file mode 100644
index 000000000000..413028afad19
Binary files /dev/null and b/packages/manager/apps/hpc-backup-licenses/src/assets/error-banner-oops.png differ
diff --git a/packages/manager/apps/hpc-backup-licenses/src/components/.gitkeep b/packages/manager/apps/hpc-backup-licenses/src/components/.gitkeep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/manager/apps/hpc-backup-licenses/src/data/.gitkeep b/packages/manager/apps/hpc-backup-licenses/src/data/.gitkeep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.spec.tsx b/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.spec.tsx
new file mode 100644
index 000000000000..743df65f71b5
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.spec.tsx
@@ -0,0 +1,87 @@
+import type { ReactNode } from 'react';
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { renderHook, waitFor } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import type { OnboardingConfigType } from '@/types/Onboarding.type';
+
+import { useGuideLinks, useOnboardingContent } from './useOnboardingData';
+
+// --- Mock App.constants ---
+vi.mock('@/App.constants', () => {
+ const mockConfig: OnboardingConfigType = {
+ productName: 'MyProduct',
+ productCategory: 'CategoryX',
+ brand: 'BrandY',
+ title: 'Welcome to MyProduct',
+ heroImage: { src: '/hero.png', alt: 'Hero' },
+ tiles: [{ id: 1, key: 'discover', linkKey: 'discover' }],
+ links: { discover: '/discover', tutorial: '/tutorial', faq: '/faq' },
+ };
+ return { ONBOARDING_CONFIG: mockConfig };
+});
+
+// --- Mock translation ---
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string, opts?: { productName?: string }) =>
+ `${key}${opts?.productName ? `:${opts.productName}` : ''}`,
+ }),
+}));
+
+// --- Helper wrapper with React Query ---
+const createWrapper =
+ () =>
+ // eslint-disable-next-line react/display-name
+ ({ children }: { children: ReactNode }) => {
+ const client = new QueryClient();
+ return {children};
+ };
+
+describe('useOnboardingContent', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('returns onboarding content with hero image and tiles', async () => {
+ const { result } = renderHook(() => useOnboardingContent(), { wrapper: createWrapper() });
+
+ await waitFor(() => {
+ expect(result.current.productName).toBe('MyProduct');
+ expect(result.current.productCategory).toBe('CategoryX');
+ expect(result.current.brand).toBe('BrandY');
+ expect(result.current.heroImage?.src).toBe('/hero.png');
+ expect(result.current.tiles).toHaveLength(1);
+ });
+ });
+
+ it('falls back when productName is missing', async () => {
+ vi.doMock('@/App.constants', () => ({
+ ONBOARDING_CONFIG: {
+ productCategory: 'X',
+ brand: 'Y',
+ tiles: [],
+ links: {},
+ } as unknown as Partial,
+ }));
+
+ const { result } = renderHook(() => useOnboardingContent(), { wrapper: createWrapper() });
+
+ await waitFor(() => {
+ expect(result.current.productName).toContain('onboarding:productDefaultName');
+ });
+ });
+});
+
+describe('useGuideLinks', () => {
+ it('returns guide links correctly', async () => {
+ const { result } = renderHook(() => useGuideLinks(), { wrapper: createWrapper() });
+
+ await waitFor(() => {
+ expect(result.current.discover).toBe('/discover');
+ expect(result.current.tutorial).toBe('/tutorial');
+ expect(result.current.faq).toBe('/faq');
+ });
+ });
+});
diff --git a/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.tsx b/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.tsx
new file mode 100644
index 000000000000..6d37355792ae
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.tsx
@@ -0,0 +1,64 @@
+import { useMemo } from 'react';
+
+import { useQuery } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
+
+import * as AppConfig from '@/App.constants';
+import type {
+ OnboardingConfigType,
+ OnboardingContentType,
+ OnboardingImage,
+ OnboardingLinksType,
+} from '@/types/Onboarding.type';
+
+const QUERY_KEY = ['onboarding-config'] as const;
+
+async function getOnboardingConfig(): Promise {
+ return new Promise((resolve) => {
+ resolve(AppConfig.ONBOARDING_CONFIG);
+ });
+}
+
+export function useOnboardingContent(): OnboardingContentType {
+ const { t } = useTranslation(['onboarding', 'common']);
+ const { data } = useQuery({
+ queryKey: QUERY_KEY,
+ queryFn: () => getOnboardingConfig(),
+ });
+
+ return useMemo(() => {
+ const productName = data?.productName ?? t('onboarding:productDefaultName', 'Product');
+
+ const heroImage: OnboardingImage | undefined = data?.heroImage?.src
+ ? {
+ src: data.heroImage.src,
+ alt: data.heroImage.alt ?? t('onboarding:heroAlt', { productName }),
+ }
+ : undefined;
+
+ return {
+ productName,
+ productCategory: data?.productCategory,
+ brand: data?.brand,
+ title: data?.title,
+ heroImage,
+ tiles: Array.isArray(data?.tiles) ? data.tiles : [],
+ };
+ }, [data, t]);
+}
+
+export function useGuideLinks(): OnboardingLinksType {
+ const { data } = useQuery({
+ queryKey: QUERY_KEY,
+ queryFn: () => getOnboardingConfig(),
+ });
+
+ return useMemo(
+ () => ({
+ discover: data?.links?.discover ?? '',
+ tutorial: data?.links?.tutorial ?? '',
+ faq: data?.links?.faq ?? '',
+ }),
+ [data],
+ );
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/src/i18n.ts b/packages/manager/apps/hpc-backup-licenses/src/i18n.ts
new file mode 100644
index 000000000000..3dfc6a85f8fe
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/i18n.ts
@@ -0,0 +1,27 @@
+import i18n from 'i18next';
+import I18NextHttpBackend from 'i18next-http-backend';
+import { initReactI18next } from 'react-i18next';
+
+export default async function initI18n(locale = 'fr_FR', availablesLocales = ['fr_FR']) {
+ await i18n
+ .use(initReactI18next)
+ .use(I18NextHttpBackend)
+ .use({
+ type: 'postProcessor',
+ name: 'normalize',
+ process: (value: string) => (value ? value.replace(/&/g, '&') : value),
+ })
+ .init({
+ lng: locale,
+ fallbackLng: 'fr_FR',
+ supportedLngs: availablesLocales,
+ defaultNS: 'common',
+ ns: ['common'], // namespaces to load by default
+ backend: {
+ loadPath: (lngs: string[], namespaces: string[]) =>
+ `${import.meta.env.BASE_URL}translations/${namespaces[0]}/Messages_${lngs[0]}.json`,
+ },
+ postProcess: 'normalize',
+ });
+ return i18n;
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/src/index.scss b/packages/manager/apps/hpc-backup-licenses/src/index.scss
new file mode 100644
index 000000000000..a5c684d9dcdd
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/index.scss
@@ -0,0 +1,8 @@
+@tailwind utilities;
+
+@import '@ovhcloud/ods-themes/default';
+@import '@ovh-ux/manager-react-components/dist/style.css';
+
+html {
+ font-family: var(--ods-font-family-default);
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/src/index.tsx b/packages/manager/apps/hpc-backup-licenses/src/index.tsx
new file mode 100644
index 000000000000..1a4b0f0b63d3
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/index.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+
+import ReactDOM from 'react-dom/client';
+
+import { ShellContext, initI18n, initShellContext } from '@ovh-ux/manager-react-shell-client';
+
+import { appName } from '@/App.constants';
+
+import App from './App';
+import { APP_NAME, LEVEL2, SUB_UNIVERSE, UNIVERSE } from './Tracking.constants';
+import './index.scss';
+import './vite-hmr';
+
+const trackingContext = {
+ chapter1: UNIVERSE,
+ chapter2: SUB_UNIVERSE,
+ chapter3: APP_NAME,
+ appName: APP_NAME,
+ pageTheme: UNIVERSE,
+ level2Config: LEVEL2,
+};
+
+const init = async (appName: string) => {
+ // Initialize shell context: auth, env, tracking, APIs
+ const context = await initShellContext(appName, trackingContext);
+
+ // Initialize i18n with default namespaces
+ await initI18n({
+ context,
+ reloadOnLocaleChange: true,
+ defaultNS: appName,
+ ns: ['listing', 'dashboard', 'onboarding'],
+ });
+
+ // Configure tracking per region
+ const region = context.environment.getRegion();
+ context.shell.tracking.setConfig(region, LEVEL2);
+
+ // Try loading region-specific runtime config (optional)
+ try {
+ await import(`./config-${region}.js`);
+ } catch {
+ // No-op when region config is absent
+ }
+
+ // Mount React app
+ const rootElement = document.getElementById('root');
+ if (!rootElement) {
+ throw new Error('Root element #root not found');
+ }
+
+ ReactDOM.createRoot(rootElement).render(
+
+
+
+
+ ,
+ );
+};
+
+// Start the app
+void init(appName);
diff --git a/packages/manager/apps/hpc-backup-licenses/src/pages/Main.layout.tsx b/packages/manager/apps/hpc-backup-licenses/src/pages/Main.layout.tsx
new file mode 100644
index 000000000000..6014bcdf9ffd
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/pages/Main.layout.tsx
@@ -0,0 +1,42 @@
+import { Suspense, useContext, useEffect } from 'react';
+
+import { Outlet, useLocation, useMatches } from 'react-router-dom';
+
+import { ShellContext, useOvhTracking, useRouteSynchro } from '@ovh-ux/manager-react-shell-client';
+import { defineCurrentPage } from '@ovh-ux/request-tagger';
+
+import { appName } from '@/App.constants';
+
+export default function MainLayout() {
+ const location = useLocation();
+ const matches = useMatches();
+ const { shell } = useContext(ShellContext);
+ const { trackCurrentPage } = useOvhTracking();
+
+ // Keep shell route state in sync with React Router
+ useRouteSynchro();
+
+ // Tag the current page for request correlation
+ useEffect(() => {
+ const lastMatch = matches.at(-1); // most specific route
+ if (lastMatch?.id) {
+ defineCurrentPage(`app.${appName}-${lastMatch.id}`);
+ }
+ }, [matches]);
+
+ // Track page views on route change
+ useEffect(() => {
+ trackCurrentPage();
+ }, [trackCurrentPage, location.pathname]);
+
+ // Hide shell preloader on mount
+ useEffect(() => {
+ void shell?.ux.hidePreloader();
+ }, [shell]);
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/src/pages/not-found/404.page.tsx b/packages/manager/apps/hpc-backup-licenses/src/pages/not-found/404.page.tsx
new file mode 100644
index 000000000000..d052f1ebcbbf
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/pages/not-found/404.page.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+export default function NotFound() {
+ // @TODO: add a redirection here in order to catch /:serviceName given from iframe
+
+ return 404 - route not found
;
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/Onboarding.page.tsx b/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/Onboarding.page.tsx
new file mode 100644
index 000000000000..9fba1e655118
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/Onboarding.page.tsx
@@ -0,0 +1,83 @@
+import React, { useMemo } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { OdsText } from '@ovhcloud/ods-components/react';
+
+import { NAMESPACES } from '@ovh-ux/manager-common-translations';
+import { Card, OnboardingLayout } from '@ovh-ux/manager-react-components';
+
+import { useGuideLinks, useOnboardingContent } from '@/hooks/onboarding/useOnboardingData';
+import { OnboardingLinksType } from '@/types/Onboarding.type';
+
+export default function OnboardingPage() {
+ const { t } = useTranslation(['onboarding', NAMESPACES.ACTIONS, NAMESPACES.ONBOARDING]);
+ const { productName, productCategory, brand, title, heroImage, tiles } = useOnboardingContent();
+ const links: OnboardingLinksType = useGuideLinks();
+
+ // Build localized description paragraph.
+ const description = useMemo(
+ () => (
+
+
+ {t('onboarding:description_part1', {
+ productName,
+ productCategory,
+ brand,
+ })}
+
+ {t('onboarding:description_part2', { productName })}
+
+ ),
+ [t, productName, productCategory, brand],
+ );
+
+ // Build hero image object with fallback alt text.
+ const img = useMemo(
+ () =>
+ heroImage
+ ? {
+ src: heroImage.src,
+ alt: heroImage.alt ?? t('onboarding:hero_alt', { productName }),
+ }
+ : undefined,
+ [heroImage, t, productName],
+ );
+
+ // Filter tiles to include only those with matching guide links.
+ const validTiles = useMemo(
+ () => tiles.filter(({ linkKey }) => Boolean(links[linkKey])),
+ [tiles, links],
+ );
+
+ return (
+ {}}
+ moreInfoButtonLabel={t(`${NAMESPACES.ONBOARDING}:more_infos`)}
+ >
+ {validTiles.map(({ id, key, linkKey }) => {
+ const href = links[linkKey];
+ if (!href) return null;
+
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/__tests__/Onboarding.spec.tsx b/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/__tests__/Onboarding.spec.tsx
new file mode 100644
index 000000000000..d76df62ce498
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/__tests__/Onboarding.spec.tsx
@@ -0,0 +1,140 @@
+import React from 'react';
+
+import { render, screen } from '@testing-library/react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { CardProps, OnboardingLayoutProps } from '@ovh-ux/manager-react-components';
+
+import type { OnboardingContentType, OnboardingLinksType } from '@/types/Onboarding.type';
+
+import OnboardingPage from '../Onboarding.page';
+
+type MockUseOnboardingDataModule = {
+ useOnboardingContent: () => OnboardingContentType;
+ useGuideLinks: () => OnboardingLinksType;
+};
+
+vi.mock('react-i18next', () => ({
+ useTranslation: (): {
+ t: (key: string, vars?: Record) => string;
+ } => ({
+ t: (key: string, vars?: Record) => `${key}${vars ? JSON.stringify(vars) : ''}`,
+ }),
+}));
+
+vi.mock(
+ '@/hooks/onboarding/useOnboardingData',
+ (): MockUseOnboardingDataModule => ({
+ useOnboardingContent: () => ({
+ productName: 'Web Hosting',
+ productCategory: 'Hosting',
+ brand: 'OVHcloud',
+ title: 'Welcome to Web Hosting',
+ heroImage: { src: 'hero.jpg', alt: 'Hero Image' },
+ tiles: [
+ { id: 1, key: 'discover', linkKey: 'discover' },
+ { id: 2, key: 'tutorial', linkKey: 'tutorial' },
+ { id: 3, key: 'faq', linkKey: 'faq' },
+ ],
+ }),
+ useGuideLinks: () => ({
+ discover: 'https://guides.ovh.com/discover',
+ tutorial: 'https://guides.ovh.com/tutorial',
+ faq: 'https://guides.ovh.com/faq',
+ }),
+ }),
+);
+
+vi.mock('@ovh-ux/manager-react-components', () => {
+ const OnboardingLayout = ({
+ title,
+ img,
+ description,
+ children,
+ }: OnboardingLayoutProps): JSX.Element => (
+
+
{title}
+ {img &&

}
+
{description}
+
{children}
+
+ );
+
+ const Card = ({ href, texts }: CardProps): JSX.Element => (
+
+ {texts.title}
+ {texts.description}
+ {texts.category}
+
+ );
+
+ return { OnboardingLayout, Card };
+});
+
+describe('OnboardingPage', () => {
+ afterEach(() => {
+ vi.resetModules();
+ });
+
+ it('renders layout with translated title, description, hero image and link cards', () => {
+ render();
+
+ // Layout + title
+ const layout = screen.getByTestId('layout');
+ expect(layout).toBeInTheDocument();
+
+ const title = screen.getByRole('heading', { level: 1 });
+ expect(title).toHaveTextContent('Welcome to Web Hosting');
+
+ // Hero image
+ const img = screen.getByTestId('hero-img');
+ expect(img).toHaveAttribute('src', 'hero.jpg');
+ expect(img).toHaveAttribute('alt', 'Hero Image');
+
+ // Description section
+ expect(screen.getByTestId('description')).toBeInTheDocument();
+
+ // Link cards
+ const cards = screen.getAllByTestId('link-card');
+ expect(cards).toHaveLength(3);
+ expect(cards[0]).toHaveAttribute('href', 'https://guides.ovh.com/discover');
+ expect(cards[1]).toHaveAttribute('href', 'https://guides.ovh.com/tutorial');
+ expect(cards[2]).toHaveAttribute('href', 'https://guides.ovh.com/faq');
+ });
+
+ it('renders fallback title and no hero image when data missing', async () => {
+ vi.resetModules(); // clear previous imports
+
+ vi.doMock(
+ '@/hooks/onboarding/useOnboardingData',
+ (): MockUseOnboardingDataModule => ({
+ useOnboardingContent: () => ({
+ productName: 'Cloud',
+ productCategory: undefined,
+ brand: undefined,
+ title: undefined,
+ heroImage: undefined,
+ tiles: [],
+ }),
+ useGuideLinks: () => ({
+ discover: '',
+ tutorial: '',
+ faq: '',
+ }),
+ }),
+ );
+
+ // 👇 re-import component after mocks are applied
+ const { default: OnboardingPageDynamic } = await import('../Onboarding.page');
+ render();
+
+ const layout = screen.getByTestId('layout');
+ expect(layout).toBeInTheDocument();
+
+ const title = screen.getByRole('heading', { level: 1 });
+ expect(title).toHaveTextContent('onboarding:title_fallback{"productName":"Cloud"}');
+
+ expect(screen.queryByTestId('hero-img')).not.toBeInTheDocument();
+ expect(screen.queryAllByTestId('link-card')).toHaveLength(0);
+ });
+});
diff --git a/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.constants.ts b/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.constants.ts
new file mode 100644
index 000000000000..ab5780a00cc6
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.constants.ts
@@ -0,0 +1,14 @@
+import { appName } from '@/App.constants';
+
+const ROOT_URL = `/`;
+
+export const subRoutes = {
+ onboarding: 'onboarding' as const,
+} as const;
+
+export const urls = {
+ root: ROOT_URL,
+ onboarding: `${ROOT_URL}${subRoutes.onboarding}`,
+} as const;
+
+export const redirectionApp = appName;
diff --git a/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.tsx b/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.tsx
new file mode 100644
index 000000000000..b0bad7c57cec
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import { Navigate, Route } from 'react-router-dom';
+
+import { ErrorBoundary } from '@ovh-ux/manager-react-components';
+import { PageType } from '@ovh-ux/manager-react-shell-client';
+
+import NotFound from '@/pages/not-found/404.page';
+
+import { redirectionApp, subRoutes, urls } from './Routes.constants';
+
+const MainLayoutPage = React.lazy(() => import('@/pages/Main.layout'));
+const OnboardingPage = React.lazy(() => import('@/pages/onboarding/Onboarding.page'));
+
+export default (
+ <>
+ } />
+
+ }
+ >
+
+ } />
+
+ >
+);
diff --git a/packages/manager/apps/hpc-backup-licenses/src/types/Onboarding.type.ts b/packages/manager/apps/hpc-backup-licenses/src/types/Onboarding.type.ts
new file mode 100644
index 000000000000..ca34ad80483e
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/types/Onboarding.type.ts
@@ -0,0 +1,29 @@
+export type OnboardingLinksType = {
+ discover: string;
+ tutorial: string;
+ faq: string;
+};
+
+export type OnboardingTile = {
+ id: number;
+ key: string;
+ linkKey: keyof OnboardingLinksType;
+};
+
+export type OnboardingImage = {
+ src: string;
+ alt?: string;
+};
+
+export type OnboardingContentType = {
+ productName: string;
+ productCategory?: string;
+ brand?: string;
+ title?: string;
+ heroImage?: OnboardingImage;
+ tiles: OnboardingTile[];
+};
+
+export type OnboardingConfigType = OnboardingContentType & {
+ links: OnboardingLinksType;
+};
diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/Test.utils.tsx b/packages/manager/apps/hpc-backup-licenses/src/utils/Test.utils.tsx
new file mode 100644
index 000000000000..e30c73f397f0
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/utils/Test.utils.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+
+import { render } from '@testing-library/react';
+
+import { urls } from '@/routes/Routes.constants';
+import { TestApp } from '@/utils/tests/TestApp';
+import { setupMswMock } from '@/utils/tests/setupMsw';
+import { testWrapperBuilder } from '@/utils/tests/testWrapperBuilder';
+
+const formatSafePath = (url?: string) =>
+ url && url.startsWith(urls.root) ? url : `${urls.root}${url ?? ''}`;
+
+export const renderTest = async ({
+ initialRoute,
+}: {
+ initialRoute?: string;
+} = {}): Promise> => {
+ setupMswMock();
+
+ const wrapper = await testWrapperBuilder()
+ .withQueryClient()
+ .withI18next()
+ .withShellContext()
+ .build();
+
+ return render(, {
+ wrapper,
+ }) as unknown as ReturnType;
+};
diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/tests/TestApp.tsx b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/TestApp.tsx
new file mode 100644
index 000000000000..848050a8e935
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/TestApp.tsx
@@ -0,0 +1,19 @@
+import React, { Suspense } from 'react';
+
+import { RouterProvider, createMemoryRouter, createRoutesFromElements } from 'react-router-dom';
+
+import Routes from '@/routes/Routes';
+
+export function TestApp({ initialRoute = '/' }: { initialRoute: string }) {
+ const routes = createRoutesFromElements(Routes);
+ const router = createMemoryRouter(routes, {
+ initialEntries: [initialRoute],
+ initialIndex: 0,
+ });
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/tests/i18ntest.utils.ts b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/i18ntest.utils.ts
new file mode 100644
index 000000000000..8bb6162f2e76
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/i18ntest.utils.ts
@@ -0,0 +1,45 @@
+import i18next, { InitOptions, i18n } from 'i18next';
+
+import { appName } from '@/App.constants';
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+import onboarding from '../../../public/translations/onboarding/Messages_fr_FR.json';
+
+export const defaultLocale = 'fr_FR';
+export const defaultAvailableLocales = [defaultLocale];
+function addTranslations() {
+ i18next.addResources(defaultLocale, 'onboarding', onboarding).use({
+ type: 'postProcessor',
+ name: 'normalize',
+ process: (value: string) => (value ? value.replace(/&/g, '&') : value),
+ });
+}
+
+export const getTesti18nParams = (): InitOptions => ({
+ lng: defaultLocale,
+ defaultNS: appName,
+ ns: [],
+ supportedLngs: defaultAvailableLocales,
+ postProcess: 'normalize',
+ interpolation: {
+ escapeValue: false,
+ },
+});
+
+export const initTestI18n = () =>
+ new Promise((resolve) => {
+ void i18next.init(getTesti18nParams());
+ if (i18next.isInitialized) {
+ addTranslations();
+ } else {
+ i18next.on('initialized', () => {
+ addTranslations();
+ resolve(i18next);
+ });
+ }
+ });
+
+export const labels = {
+ onboarding,
+};
diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/tests/setupMsw.ts b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/setupMsw.ts
new file mode 100644
index 000000000000..ea5d6633a88c
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/setupMsw.ts
@@ -0,0 +1 @@
+export const setupMswMock = () => {};
diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperBuilder.tsx b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperBuilder.tsx
new file mode 100644
index 000000000000..a181c0aa3649
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperBuilder.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+
+import {
+ TestProvider,
+ addI18nextProvider,
+ addQueryClientProvider,
+ addShellContextProvider,
+ createProviderWrapper,
+} from './testWrapperProviders';
+
+type BuilderConfig = {
+ withI18next: boolean;
+ withQueryClient: boolean;
+ withShellContext: boolean;
+};
+
+type TestWrapperBuilder = {
+ withI18next: () => TestWrapperBuilder;
+ withQueryClient: () => TestWrapperBuilder;
+ withShellContext: () => TestWrapperBuilder;
+ build: () => Promise>;
+};
+
+export const testWrapperBuilder = (): TestWrapperBuilder => {
+ const config: BuilderConfig = {
+ withI18next: false,
+ withQueryClient: false,
+ withShellContext: false,
+ };
+ const build = async (): Promise> => {
+ const providers: TestProvider[] = [];
+ if (config.withI18next) await addI18nextProvider(providers);
+ if (config.withQueryClient) addQueryClientProvider(providers);
+ if (config.withShellContext) await addShellContextProvider(providers);
+ return createProviderWrapper(providers);
+ };
+ const builder = {
+ withI18next: () => {
+ config.withI18next = true;
+ return builder;
+ },
+ withQueryClient: () => {
+ config.withQueryClient = true;
+ return builder;
+ },
+ withShellContext: () => {
+ config.withShellContext = true;
+ return builder;
+ },
+ build,
+ };
+ return builder;
+};
diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperProviders.tsx b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperProviders.tsx
new file mode 100644
index 000000000000..aa12cdbeb5e9
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperProviders.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { i18n } from 'i18next';
+import { I18nextProvider } from 'react-i18next';
+
+import {
+ ShellContext,
+ ShellContextType,
+ initShellContext,
+} from '@ovh-ux/manager-react-shell-client';
+
+import { appName } from '@/App.constants';
+
+import { initTestI18n } from './i18ntest.utils';
+
+export type TestProvider = (props: React.PropsWithChildren) => React.ReactNode;
+
+let context: ShellContextType;
+let i18nState: i18n;
+
+export const createProviderWrapper = (
+ providers: TestProvider[],
+): React.FC => {
+ return ({ children }) =>
+ providers.reduceRight((acc, provider) => {
+ const ProviderComponent = provider;
+ return {acc};
+ }, children);
+};
+
+export const addI18nextProvider = async (providers: TestProvider[]) => {
+ if (!i18nState) {
+ i18nState = await initTestI18n();
+ }
+ providers.push(({ children }) => {children});
+};
+
+export const addQueryClientProvider = (providers: TestProvider[]) => {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
+ });
+ providers.push(({ children }) => (
+ {children}
+ ));
+};
+
+export const addShellContextProvider = async (providers: TestProvider[]) => {
+ if (!context) {
+ context = await initShellContext(appName);
+ }
+ providers.push(({ children }) => (
+ {children}
+ ));
+};
diff --git a/packages/manager/apps/hpc-backup-licenses/src/vite-hmr.ts b/packages/manager/apps/hpc-backup-licenses/src/vite-hmr.ts
new file mode 100644
index 000000000000..473d87630039
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/src/vite-hmr.ts
@@ -0,0 +1,5 @@
+if (import.meta.hot) {
+ import.meta.hot.on('iframe-reload', () => {
+ window.location.reload();
+ });
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/tailwind.config.mjs b/packages/manager/apps/hpc-backup-licenses/tailwind.config.mjs
new file mode 100644
index 000000000000..00ed2ec80677
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/tailwind.config.mjs
@@ -0,0 +1,25 @@
+import { createRequire } from 'node:module';
+import path from 'node:path';
+
+import baseConfig from '@ovh-ux/manager-tailwind-config';
+
+const require = createRequire(import.meta.url);
+const pkgDir = (name) => path.dirname(require.resolve(`${name}/package.json`));
+const toGlob = (dir) => `${dir.replace(/\\/g, '/')}/**/*.{js,jsx,ts,tsx}`;
+
+const reactComponentsDir = pkgDir('@ovh-ux/muk');
+
+const baseTailwindConfig = [
+ ...(baseConfig.content ?? []),
+ './src/**/*.{js,jsx,ts,tsx}',
+ toGlob(reactComponentsDir),
+];
+
+/** @type {import('tailwindcss').Config} */
+export default {
+ ...baseConfig,
+ content: baseTailwindConfig,
+ corePlugins: {
+ preflight: false,
+ },
+};
diff --git a/packages/manager/apps/hpc-backup-licenses/tsconfig.json b/packages/manager/apps/hpc-backup-licenses/tsconfig.json
new file mode 100644
index 000000000000..dbe36ca19aa8
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "@ovh-ux/manager-static-analysis-kit/tsconfig/react-strict",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/public/*": ["./public/*"],
+ "@/*": ["./src/*"]
+ },
+ "outDir": "dist",
+ "resolveJsonModule": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist", "types", "src/__tests__"]
+}
diff --git a/packages/manager/apps/hpc-backup-licenses/vite.config.mjs b/packages/manager/apps/hpc-backup-licenses/vite.config.mjs
new file mode 100644
index 000000000000..57f62cb969c5
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/vite.config.mjs
@@ -0,0 +1,9 @@
+import { resolve } from 'node:path';
+import { defineConfig } from 'vite';
+
+import { getBaseConfig } from '@ovh-ux/manager-vite-config';
+
+export default defineConfig({
+ ...getBaseConfig(),
+ root: resolve(process.cwd()),
+});
diff --git a/packages/manager/apps/hpc-backup-licenses/vitest.config.js b/packages/manager/apps/hpc-backup-licenses/vitest.config.js
new file mode 100644
index 000000000000..50b7c0fd7acc
--- /dev/null
+++ b/packages/manager/apps/hpc-backup-licenses/vitest.config.js
@@ -0,0 +1,43 @@
+/* eslint-disable no-undef */
+import path from 'node:path';
+
+import {
+ createConfig,
+ defaultDedupedDependencies,
+ defaultExcludedFiles,
+ mergeConfig,
+ sharedConfig,
+ sharedCssConfig,
+} from '@ovh-ux/manager-tests-setup';
+
+export default mergeConfig(
+ sharedConfig,
+ createConfig({
+ test: {
+ setupFiles: ['@ovh-ux/manager-core-test-utils/src/utils/setup-test.ts'],
+ ...sharedCssConfig,
+ coverage: {
+ exclude: [
+ ...defaultExcludedFiles,
+ // App-specific exclusions (not in shared config):
+ 'vite-*.ts',
+ 'App.tsx',
+ '__mocks__',
+ '**/Routes.tsx',
+ '**/QueryClient.ts',
+ '**/*.type.ts',
+ '**/404.page.tsx',
+ '**/utils/tests/**',
+ '**/Test.utils.tsx',
+ ],
+ },
+ },
+ resolve: {
+ dedupe: [...defaultDedupedDependencies],
+ alias: {
+ '@/public': path.resolve(__dirname, 'public'),
+ '@': path.resolve(__dirname, 'src'),
+ },
+ },
+ }),
+);
diff --git a/packages/manager/apps/identity-access-management/src/test-utils/uiTestHelpers.tsx b/packages/manager/apps/identity-access-management/src/test-utils/uiTestHelpers.tsx
index d5e9ca88f211..616b9e985f23 100644
--- a/packages/manager/apps/identity-access-management/src/test-utils/uiTestHelpers.tsx
+++ b/packages/manager/apps/identity-access-management/src/test-utils/uiTestHelpers.tsx
@@ -61,16 +61,15 @@ export const getOdsCheckbox = async ({
disabled?: boolean;
nth?: number;
}) => {
- const checkbox: HTMLOdsCheckboxElement = Array.from(
- container.querySelectorAll('ods-checkbox'),
- )?.[nth] as HTMLOdsCheckboxElement;
+ // `OdsCheckbox` is mocked as a native `` in setupTests.tsx
+ const checkbox: HTMLInputElement = Array.from(
+ container.querySelectorAll('input[type="checkbox"]'),
+ )?.[nth] as HTMLInputElement;
expect(checkbox).toBeDefined();
- const disabledAttribute = checkbox.getAttribute('is-disabled') === 'true';
-
- if (disabled) expect(disabledAttribute).toBeTruthy();
- else expect(disabledAttribute).toBeFalsy();
+ if (disabled) expect(checkbox.disabled).toBeTruthy();
+ else expect(checkbox.disabled).toBeFalsy();
return checkbox;
};
diff --git a/packages/manager/apps/network-vrack/package.json b/packages/manager/apps/network-vrack/package.json
index 8febc09106fa..9b00b9ef5a52 100644
--- a/packages/manager/apps/network-vrack/package.json
+++ b/packages/manager/apps/network-vrack/package.json
@@ -49,6 +49,8 @@
"@ovh-ux/manager-tests-setup": "^0.5.0",
"@ovh-ux/manager-vite-config": "*",
"@tanstack/react-query-devtools": "5.51.21",
+ "@types/react": "18.3.18",
+ "@types/react-dom": "18.3.0",
"autoprefixer": "10.4.24",
"msw": "2.1.7",
"tailwindcss": "^3.4.4",
diff --git a/packages/manager/apps/pci-workflow/package.json b/packages/manager/apps/pci-workflow/package.json
index ab1793908ce6..279521bc3f5b 100644
--- a/packages/manager/apps/pci-workflow/package.json
+++ b/packages/manager/apps/pci-workflow/package.json
@@ -32,6 +32,7 @@
"@tanstack/react-query": "5.51.21",
"@tanstack/react-query-devtools": "5.51.21",
"@tanstack/react-table": "8.20.1",
+ "axios": "1.12.0",
"date-fns": "^3.6.0",
"element-internals-polyfill": "^1.3.11",
"i18next": "23.8.2",
diff --git a/packages/manager/apps/web-domains/package.json b/packages/manager/apps/web-domains/package.json
index 897398e86de7..84fbaa3dd15f 100644
--- a/packages/manager/apps/web-domains/package.json
+++ b/packages/manager/apps/web-domains/package.json
@@ -57,7 +57,9 @@
"@ovh-ux/manager-tests-setup": "^0.8.0",
"@ovh-ux/manager-vite-config": "^0.15.6",
"@tanstack/react-query-devtools": "^5.90.2",
- "@types/punycode": "^2.1.4"
+ "@types/punycode": "^2.1.4",
+ "@types/react": "18.3.18",
+ "@types/react-dom": "18.3.0"
},
"regions": [
"EU",
diff --git a/packages/manager/apps/web-ongoing-operations/package.json b/packages/manager/apps/web-ongoing-operations/package.json
index b144e0b845c3..fb7f1ce1b805 100644
--- a/packages/manager/apps/web-ongoing-operations/package.json
+++ b/packages/manager/apps/web-ongoing-operations/package.json
@@ -51,7 +51,9 @@
"@ovh-ux/manager-static-analysis-kit": "^0.15.0",
"@ovh-ux/manager-tests-setup": "^0.8.0",
"@ovh-ux/manager-vite-config": "^0.15.6",
- "@tanstack/react-query-devtools": "^5.90.2"
+ "@tanstack/react-query-devtools": "^5.90.2",
+ "@types/react": "18.3.18",
+ "@types/react-dom": "18.3.0"
},
"regions": [
"EU",
diff --git a/yarn.lock b/yarn.lock
index 5887691fe87c..b00b8ec98552 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6655,15 +6655,15 @@
i18next "^23.8.2"
i18next-http-backend "^2.4.2"
-"@ovh-ux/manager-react-shell-client@^1.2.3":
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/@ovh-ux/manager-react-shell-client/-/manager-react-shell-client-1.2.3.tgz#31b7b1297eb192099e1e3d799b02dc5c04c1db49"
- integrity sha512-n/FcrTcrki46yzP++tXmrp5+rlOFMl+BaBHrRbf6bvA+usBiUj9GbG5dewigIbnUDOegNzC2Ksrrx2EDU8SGzA==
+"@ovh-ux/manager-react-shell-client@^1.2.4":
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/@ovh-ux/manager-react-shell-client/-/manager-react-shell-client-1.2.4.tgz#336ae9acfd5d7e676d9cd0d1f772fc8c25702469"
+ integrity sha512-3tOTpvQvrT+/4H1ONbpO1V3OiS3Uha03ueAmljWH7AvNQRC+KAa4MVYoCFtHvkxTE0EyFet9YSwhrX33ZdH1ew==
dependencies:
"@ovh-ux/manager-config" "^8.9.0"
- "@ovh-ux/ovh-at-internet" "^0.29.4"
+ "@ovh-ux/ovh-at-internet" "^0.30.0"
"@ovh-ux/request-tagger" "^0.6.0"
- "@ovh-ux/shell" "^4.10.6"
+ "@ovh-ux/shell" "^4.10.7"
"@ovh-ux/url-builder" "^2.4.3"
i18next "^23.8.2"
i18next-http-backend "^2.4.2"