diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..b9e5b64
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,72 @@
+# FavMasToKey - Configuration Apache
+
+# Activer le moteur de réécriture
+RewriteEngine On
+
+# Protéger les fichiers sensibles
+<FilesMatch "^(config\.php|functions\.php)$">
+    Order Allow,Deny
+    Deny from all
+</FilesMatch>
+
+# Bloquer l'accès au répertoire includes
+<IfModule mod_rewrite.c>
+    RewriteRule ^includes/ - [F,L]
+</IfModule>
+
+# Bloquer l'accès aux fichiers cachés
+<FilesMatch "^\.">
+    Order Allow,Deny
+    Deny from all
+</FilesMatch>
+
+# Forcer HTTPS (décommenter en production)
+# RewriteCond %{HTTPS} !=on
+# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
+
+# Headers de sécurité
+<IfModule mod_headers.c>
+    # Empêcher le clickjacking
+    Header set X-Frame-Options "SAMEORIGIN"
+    
+    # Prévention XSS
+    Header set X-XSS-Protection "1; mode=block"
+    
+    # Empêcher le MIME sniffing
+    Header set X-Content-Type-Options "nosniff"
+    
+    # Référer policy
+    Header set Referrer-Policy "strict-origin-when-cross-origin"
+    
+    # Content Security Policy (CSP)
+    Header set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self';"
+</IfModule>
+
+# Désactiver l'affichage du contenu des répertoires
+Options -Indexes
+
+# Limiter les méthodes HTTP
+<LimitExcept GET POST HEAD>
+    Order Allow,Deny
+    Deny from all
+</LimitExcept>
+
+# PHP settings
+<IfModule mod_php.c>
+    # Désactiver l'affichage des erreurs en production
+    # php_flag display_errors Off
+    
+    # Limiter le temps d'exécution des scripts
+    php_value max_execution_time 120
+    
+    # Limiter la taille des téléchargements
+    php_value upload_max_filesize 10M
+    php_value post_max_size 10M
+    
+    # Sécuriser les cookies de session
+    php_value session.cookie_httponly 1
+    php_value session.use_only_cookies 1
+    
+    # Utiliser des cookies sécurisés en production
+    # php_value session.cookie_secure 1
+</IfModule>
\ No newline at end of file
diff --git a/callback.php b/callback.php
new file mode 100644
index 0000000..dbd47e6
--- /dev/null
+++ b/callback.php
@@ -0,0 +1,128 @@
+<?php
+/**
+ * FavMasToKey - Callback OAuth pour Misskey
+ */
+
+// Définir la constante pour inclure les fichiers
+define('FAVMASTOKEY', true);
+
+// Inclure les fichiers requis
+require_once 'includes/config.php';
+require_once 'includes/functions.php';
+
+// Vérifier si l'état est valide (protection CSRF)
+if (!isset($_GET['state']) || !isset($_SESSION['oauth_state']) || $_GET['state'] !== $_SESSION['oauth_state']) {
+    $_SESSION['messages'][] = [
+        'type' => 'danger',
+        'text' => 'Paramètre d\'état invalide. Veuillez réessayer.'
+    ];
+    header('Location: index.php');
+    exit;
+}
+
+// Vérifier si le code d'autorisation est présent
+if (!isset($_GET['code']) || empty($_GET['code'])) {
+    $_SESSION['messages'][] = [
+        'type' => 'danger',
+        'text' => 'Aucun code d\'autorisation reçu. L\'authentification a échoué ou a été annulée.'
+    ];
+    header('Location: index.php');
+    exit;
+}
+
+// Récupérer l'instance Misskey depuis la session
+if (!isset($_SESSION['misskey_instance']) || empty($_SESSION['misskey_instance'])) {
+    $_SESSION['messages'][] = [
+        'type' => 'danger',
+        'text' => 'Instance Misskey non définie. Veuillez recommencer.'
+    ];
+    header('Location: index.php');
+    exit;
+}
+
+$instance = $_SESSION['misskey_instance'];
+$code = $_GET['code'];
+
+// En production, ici nous échangerions le code contre un token d'accès
+// Pour cette version initiale, nous simulons l'échange
+
+// Simulation : générer un token fictif
+// Dans une implémentation réelle, nous appellerions l'API Misskey pour obtenir un vrai token
+$_SESSION['misskey_token'] = 'DEMO_' . bin2hex(random_bytes(16));
+
+// Dans un cas réel, nous aurions un code similaire à celui-ci:
+/*
+// Construire l'URL pour l'échange du code
+$token_url = "https://{$instance}/oauth/token";
+
+// Paramètres de la requête
+$params = [
+    'client_id' => $config['client_id'],
+    'client_secret' => $config['client_secret'],
+    'grant_type' => 'authorization_code',
+    'code' => $code,
+    'redirect_uri' => $config['app_url'] . '/callback.php'
+];
+
+// Initialiser cURL
+$ch = curl_init();
+
+// Configurer la requête
+curl_setopt_array($ch, [
+    CURLOPT_URL => $token_url,
+    CURLOPT_RETURNTRANSFER => true,
+    CURLOPT_POST => true,
+    CURLOPT_POSTFIELDS => http_build_query($params),
+    CURLOPT_HTTPHEADER => [
+        'Content-Type: application/x-www-form-urlencoded'
+    ],
+    CURLOPT_TIMEOUT => 30,
+    CURLOPT_SSL_VERIFYPEER => true
+]);
+
+// Exécuter la requête
+$response = curl_exec($ch);
+$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+$error = curl_error($ch);
+
+// Fermer la session cURL
+curl_close($ch);
+
+// Vérifier les erreurs
+if ($error || $http_code !== 200) {
+    $_SESSION['messages'][] = [
+        'type' => 'danger',
+        'text' => 'Erreur lors de l\'échange du code d\'autorisation: ' . ($error ?: 'HTTP ' . $http_code)
+    ];
+    header('Location: index.php');
+    exit;
+}
+
+// Décoder la réponse
+$response_data = json_decode($response, true);
+
+// Vérifier que le token est présent
+if (!isset($response_data['access_token']) || empty($response_data['access_token'])) {
+    $_SESSION['messages'][] = [
+        'type' => 'danger',
+        'text' => 'Aucun token d\'accès reçu. L\'authentification a échoué.'
+    ];
+    header('Location: index.php');
+    exit;
+}
+
+// Stocker le token dans la session
+$_SESSION['misskey_token'] = $response_data['access_token'];
+*/
+
+// Nettoyer l'état OAuth
+unset($_SESSION['oauth_state']);
+
+// Ajouter un message de succès
+$_SESSION['messages'][] = [
+    'type' => 'success',
+    'text' => 'Connecté avec succès à ' . $instance . '.'
+];
+
+// Rediriger vers la page de migration (étape 3)
+header('Location: index.php#step3');
\ No newline at end of file
diff --git a/css/styles.css b/css/styles.css
new file mode 100644
index 0000000..9a49066
--- /dev/null
+++ b/css/styles.css
@@ -0,0 +1,89 @@
+/* FavMasToKey - Styles personnalisés */
+
+body {
+    background-color: #f8f9fa;
+}
+
+.container {
+    max-width: 900px;
+}
+
+h1 {
+    color: #563d7c;
+    margin-bottom: 0.5rem;
+}
+
+.step {
+    transition: all 0.3s ease;
+}
+
+.card {
+    border-radius: 10px;
+    overflow: hidden;
+}
+
+.card-title {
+    color: #563d7c;
+    border-bottom: 1px solid #e9ecef;
+    padding-bottom: 1rem;
+    margin-bottom: 1.5rem;
+}
+
+#log-container {
+    font-family: 'Courier New', monospace;
+    font-size: 0.85rem;
+    border-radius: 5px;
+}
+
+#operation-log .log-entry {
+    margin-bottom: 0.5rem;
+}
+
+#operation-log .success {
+    color: #28a745;
+}
+
+#operation-log .error {
+    color: #dc3545;
+}
+
+#operation-log .info {
+    color: #17a2b8;
+}
+
+#operation-log .warning {
+    color: #ffc107;
+}
+
+/* Animation pour montrer le progrès */
+@keyframes progress-pulse {
+    0% { opacity: 1; }
+    50% { opacity: 0.7; }
+    100% { opacity: 1; }
+}
+
+.progress-bar.active {
+    animation: progress-pulse 2s infinite;
+}
+
+/* Styles pour les boutons */
+.btn-primary {
+    background-color: #563d7c;
+    border-color: #563d7c;
+}
+
+.btn-primary:hover, .btn-primary:focus {
+    background-color: #452d6b;
+    border-color: #452d6b;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+    .container {
+        padding: 1rem;
+    }
+    
+    .card-body {
+        padding: 1.25rem;
+    }
+}
\ No newline at end of file
diff --git a/includes/config.php b/includes/config.php
new file mode 100644
index 0000000..679b10c
--- /dev/null
+++ b/includes/config.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * FavMasToKey - Configuration
+ */
+
+// Empêcher l'accès direct au fichier
+if (!defined('FAVMASTOKEY')) {
+    die('Accès direct interdit');
+}
+
+// Environnement (development ou production)
+define('ENVIRONMENT', 'development');
+
+// Gestion des erreurs selon l'environnement
+if (ENVIRONMENT === 'development') {
+    error_reporting(E_ALL);
+    ini_set('display_errors', 1);
+} else {
+    error_reporting(0);
+    ini_set('display_errors', 0);
+}
+
+// Configuration de l'application
+$config = [
+    // Informations de l'application (à remplir lors de la création de l'app sur Misskey)
+    'app_name' => 'FavMasToKey',
+    'app_description' => 'Outil de transfert des favoris de Mastodon vers Misskey',
+    'app_version' => '0.1.0',
+    
+    // Paramètres OAuth - À CONFIGURER
+    'client_id' => '',     // Obtenus lors de l'enregistrement de votre app sur Misskey
+    'client_secret' => '', // Obtenus lors de l'enregistrement de votre app sur Misskey
+    
+    // URLs de base
+    'app_url' => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']),
+    
+    // Paramètres Misskey API
+    'misskey_api_endpoint' => '/api/notes/favorites/create',
+    
+    // Paramètres pour le traitement
+    'batch_size' => 10,    // Nombre de favoris à traiter en une fois
+    'timeout' => 30,       // Timeout des requêtes en secondes
+    'max_retries' => 3     // Nombre maximal de tentatives par favori
+];
+
+// Session
+session_start();
+
+// Fonctions utilitaires
+function debug($data) {
+    if (ENVIRONMENT === 'development') {
+        echo '<pre>';
+        print_r($data);
+        echo '</pre>';
+    }
+}
\ No newline at end of file
diff --git a/includes/functions.php b/includes/functions.php
new file mode 100644
index 0000000..7182c02
--- /dev/null
+++ b/includes/functions.php
@@ -0,0 +1,199 @@
+<?php
+/**
+ * FavMasToKey - Fonctions utilitaires
+ */
+
+// Empêcher l'accès direct au fichier
+if (!defined('FAVMASTOKEY')) {
+    die('Accès direct interdit');
+}
+
+/**
+ * Valide un fichier JSON de favoris Mastodon
+ * 
+ * @param string $file_path Chemin vers le fichier JSON
+ * @return array|bool Tableau contenant les données du fichier ou false en cas d'erreur
+ */
+function validate_mastodon_json($file_path) {
+    // Vérifier si le fichier existe
+    if (!file_exists($file_path)) {
+        return [
+            'success' => false,
+            'message' => 'Le fichier n\'existe pas.'
+        ];
+    }
+
+    // Lire le contenu du fichier
+    $content = file_get_contents($file_path);
+    if (!$content) {
+        return [
+            'success' => false,
+            'message' => 'Impossible de lire le contenu du fichier.'
+        ];
+    }
+
+    // Décoder le JSON
+    $json = json_decode($content, true);
+    if (json_last_error() !== JSON_ERROR_NONE) {
+        return [
+            'success' => false,
+            'message' => 'Le fichier n\'est pas un JSON valide: ' . json_last_error_msg()
+        ];
+    }
+
+    // Vérifier la structure du fichier
+    if (!isset($json['@context']) || !isset($json['type']) || !isset($json['orderedItems'])) {
+        return [
+            'success' => false,
+            'message' => 'Le format du fichier JSON n\'est pas celui attendu pour un export de favoris Mastodon.'
+        ];
+    }
+
+    // Vérifier que orderedItems est un tableau
+    if (!is_array($json['orderedItems'])) {
+        return [
+            'success' => false,
+            'message' => 'Le format des favoris dans le fichier est invalide.'
+        ];
+    }
+
+    // Tout est OK
+    return [
+        'success' => true,
+        'data' => $json,
+        'count' => count($json['orderedItems'])
+    ];
+}
+
+/**
+ * Extrait les identifiants de publications à partir des URLs Mastodon
+ * 
+ * @param array $urls Tableau d'URLs Mastodon
+ * @return array Tableau d'identifiants extraits
+ */
+function extract_toot_ids($urls) {
+    $ids = [];
+    
+    foreach ($urls as $url) {
+        // Format attendu: https://instance.tld/users/username/statuses/id
+        $parts = explode('/', $url);
+        
+        // L'ID devrait être le dernier élément après "statuses"
+        $id = end($parts);
+        
+        if (is_numeric($id)) {
+            $ids[] = [
+                'original_url' => $url,
+                'toot_id' => $id,
+                'instance' => parse_url($url, PHP_URL_HOST),
+                'username' => isset($parts[count($parts) - 3]) ? $parts[count($parts) - 3] : null
+            ];
+        }
+    }
+    
+    return $ids;
+}
+
+/**
+ * Effectue une requête cURL vers l'API Misskey
+ * 
+ * @param string $instance Instance Misskey (ex: misskey.io)
+ * @param string $endpoint Point d'accès API (ex: /api/notes/favorites/create)
+ * @param array $data Données à envoyer
+ * @param string $token Token d'accès OAuth
+ * @return array Résultat de la requête
+ */
+function misskey_api_request($instance, $endpoint, $data, $token) {
+    global $config;
+    
+    // Construire l'URL complète
+    $url = "https://{$instance}{$endpoint}";
+    
+    // Ajouter le token d'accès aux données
+    $data['i'] = $token;
+    
+    // Initialiser cURL
+    $ch = curl_init();
+    
+    // Configurer la requête
+    curl_setopt_array($ch, [
+        CURLOPT_URL => $url,
+        CURLOPT_RETURNTRANSFER => true,
+        CURLOPT_POST => true,
+        CURLOPT_POSTFIELDS => json_encode($data),
+        CURLOPT_HTTPHEADER => [
+            'Content-Type: application/json',
+            'User-Agent: FavMasToKey/' . $config['app_version']
+        ],
+        CURLOPT_TIMEOUT => $config['timeout'],
+        CURLOPT_SSL_VERIFYPEER => true
+    ]);
+    
+    // Exécuter la requête
+    $response = curl_exec($ch);
+    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+    $error = curl_error($ch);
+    
+    // Fermer la session cURL
+    curl_close($ch);
+    
+    // Vérifier les erreurs
+    if ($error) {
+        return [
+            'success' => false,
+            'message' => 'Erreur cURL: ' . $error,
+            'http_code' => $http_code
+        ];
+    }
+    
+    // Décoder la réponse
+    $response_data = json_decode($response, true);
+    
+    // Vérifier si la requête a réussi
+    if ($http_code >= 200 && $http_code < 300) {
+        return [
+            'success' => true,
+            'data' => $response_data,
+            'http_code' => $http_code
+        ];
+    } else {
+        return [
+            'success' => false,
+            'message' => isset($response_data['error']) ? $response_data['error'] : 'Erreur API Misskey',
+            'http_code' => $http_code,
+            'data' => $response_data
+        ];
+    }
+}
+
+/**
+ * Génère une URL d'autorisation OAuth pour Misskey
+ * 
+ * @param string $instance Instance Misskey
+ * @return string URL d'autorisation
+ */
+function generate_oauth_url($instance) {
+    global $config;
+    
+    // Générer un état aléatoire pour la sécurité
+    $state = bin2hex(random_bytes(16));
+    $_SESSION['oauth_state'] = $state;
+    $_SESSION['misskey_instance'] = $instance;
+    
+    // Construire l'URL de callback
+    $callback_url = $config['app_url'] . '/callback.php';
+    
+    // Paramètres de la requête d'autorisation
+    $params = [
+        'client_id' => $config['client_id'],
+        'response_type' => 'code',
+        'redirect_uri' => $callback_url,
+        'scope' => 'write:favorites',
+        'state' => $state
+    ];
+    
+    // Construire l'URL d'autorisation
+    $auth_url = "https://{$instance}/oauth/authorize?" . http_build_query($params);
+    
+    return $auth_url;
+}
\ No newline at end of file
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..a7123be
--- /dev/null
+++ b/index.php
@@ -0,0 +1,151 @@
+<?php
+/**
+ * FavMasToKey - Page d'accueil
+ */
+
+// Définir la constante pour inclure les fichiers
+define('FAVMASTOKEY', true);
+
+// Inclure les fichiers requis
+require_once 'includes/config.php';
+require_once 'includes/functions.php';
+
+// Vérifier si l'utilisateur est authentifié
+$is_authenticated = isset($_SESSION['misskey_token']) && !empty($_SESSION['misskey_token']);
+$instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : '';
+
+// Initialiser les messages
+$messages = [];
+if (isset($_SESSION['messages'])) {
+    $messages = $_SESSION['messages'];
+    unset($_SESSION['messages']);
+}
+?>
+<!DOCTYPE html>
+<html lang="fr">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>FavMasToKey - Transférer vos favoris de Mastodon vers Misskey</title>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
+    <link rel="stylesheet" href="css/styles.css">
+</head>
+<body>
+    <div class="container py-5">
+        <header class="text-center mb-5">
+            <h1>FavMasToKey</h1>
+            <p class="lead">Transférez vos favoris Mastodon vers Misskey en quelques clics</p>
+        </header>
+
+        <!-- Messages d'alerte -->
+        <?php if (!empty($messages)): ?>
+            <?php foreach ($messages as $message): ?>
+                <div class="alert alert-<?php echo $message['type']; ?> alert-dismissible fade show" role="alert">
+                    <?php echo $message['text']; ?>
+                    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Fermer"></button>
+                </div>
+            <?php endforeach; ?>
+        <?php endif; ?>
+
+        <div class="row justify-content-center">
+            <div class="col-md-8">
+                <div class="card shadow-sm">
+                    <div class="card-body">
+                        <div class="steps">
+                            <!-- Étape 1: Téléchargement du fichier JSON -->
+                            <div class="step" id="step1">
+                                <h3 class="card-title">1. Importer vos favoris Mastodon</h3>
+                                <p>Téléchargez d'abord votre fichier d'export de favoris depuis Mastodon.</p>
+                                
+                                <div class="card bg-light mb-3">
+                                    <div class="card-body">
+                                        <h5>Comment obtenir mon fichier de favoris ?</h5>
+                                        <ol>
+                                            <li>Connectez-vous à votre compte Mastodon</li>
+                                            <li>Allez dans <strong>Préférences</strong> > <strong>Exporter et importer</strong></li>
+                                            <li>Dans la section <strong>Exporter</strong>, cliquez sur <strong>Demander vos favoris</strong></li>
+                                            <li>Une fois le fichier prêt, téléchargez-le</li>
+                                        </ol>
+                                    </div>
+                                </div>
+                                
+                                <form id="upload-form" class="mt-4">
+                                    <div class="mb-3">
+                                        <label for="json-file" class="form-label">Fichier JSON des favoris</label>
+                                        <input type="file" class="form-control" id="json-file" name="json_file" accept=".json" required>
+                                    </div>
+                                    <button type="submit" class="btn btn-primary">Analyser le fichier</button>
+                                </form>
+                            </div>
+                            
+                            <!-- Étape 2: Connexion à Misskey -->
+                            <div class="step d-none" id="step2">
+                                <h3 class="card-title">2. Connexion à Misskey</h3>
+                                <p>Connectez-vous à votre compte Misskey pour y importer vos favoris.</p>
+                                
+                                <div id="file-summary" class="alert alert-info mb-4"></div>
+                                
+                                <form id="misskey-form" class="mt-4">
+                                    <div class="mb-3">
+                                        <label for="misskey-instance" class="form-label">Instance Misskey</label>
+                                        <input type="url" class="form-control" id="misskey-instance" name="misskey_instance" 
+                                               placeholder="https://misskey.io" required>
+                                        <div class="form-text">Entrez l'URL complète de votre instance Misskey (ex: https://misskey.io)</div>
+                                    </div>
+                                    <button type="submit" class="btn btn-primary">Se connecter à Misskey</button>
+                                    <button type="button" class="btn btn-link" id="back-to-step1">Retour</button>
+                                </form>
+                            </div>
+                            
+                            <!-- Étape 3: Migration des favoris -->
+                            <div class="step d-none" id="step3">
+                                <h3 class="card-title">3. Migration des favoris</h3>
+                                <p>Nous allons maintenant transférer vos favoris vers Misskey.</p>
+                                
+                                <?php if ($is_authenticated): ?>
+                                <div class="alert alert-success mb-4">
+                                    <strong>Connecté à <?php echo htmlspecialchars($instance); ?></strong>
+                                    <p class="mb-0">Vous êtes authentifié et prêt à importer vos favoris.</p>
+                                </div>
+                                <?php endif; ?>
+                                
+                                <div class="mb-4">
+                                    <label class="form-label">Progression globale</label>
+                                    <div class="progress" style="height: 20px;">
+                                        <div id="global-progress" class="progress-bar" role="progressbar" 
+                                             style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
+                                    </div>
+                                </div>
+                                
+                                <div class="mb-4">
+                                    <label class="form-label">Opération en cours</label>
+                                    <div class="progress">
+                                        <div id="current-progress" class="progress-bar bg-info" role="progressbar" 
+                                             style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
+                                    </div>
+                                </div>
+                                
+                                <div class="mb-4">
+                                    <h5>Journal des opérations</h5>
+                                    <div id="log-container" class="border p-3 bg-light" style="max-height: 200px; overflow-y: auto;">
+                                        <div id="operation-log"></div>
+                                    </div>
+                                </div>
+                                
+                                <div class="d-flex justify-content-between">
+                                    <button type="button" class="btn btn-primary" id="start-migration">Démarrer la migration</button>
+                                    <button type="button" class="btn btn-warning d-none" id="pause-migration">Pause</button>
+                                    <button type="button" class="btn btn-danger" id="cancel-migration">Annuler</button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
+    <script src="js/app.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/js/app.js b/js/app.js
new file mode 100644
index 0000000..ad9f085
--- /dev/null
+++ b/js/app.js
@@ -0,0 +1,323 @@
+/**
+ * FavMasToKey - Script JavaScript principal
+ */
+
+// Attendre que le DOM soit chargé
+document.addEventListener('DOMContentLoaded', function() {
+    // Éléments DOM
+    const uploadForm = document.getElementById('upload-form');
+    const misskeyForm = document.getElementById('misskey-form');
+    const jsonFileInput = document.getElementById('json-file');
+    const step1 = document.getElementById('step1');
+    const step2 = document.getElementById('step2');
+    const step3 = document.getElementById('step3');
+    const fileSummary = document.getElementById('file-summary');
+    const backToStep1 = document.getElementById('back-to-step1');
+    const startMigration = document.getElementById('start-migration');
+    const pauseMigration = document.getElementById('pause-migration');
+    const cancelMigration = document.getElementById('cancel-migration');
+    const globalProgress = document.getElementById('global-progress');
+    const currentProgress = document.getElementById('current-progress');
+    const operationLog = document.getElementById('operation-log');
+    
+    // Variables globales
+    let favoritesList = [];
+    let currentIndex = 0;
+    let totalItems = 0;
+    let isProcessing = false;
+    let isPaused = false;
+    
+    // Gérer le téléchargement et l'analyse du fichier JSON
+    if (uploadForm) {
+        uploadForm.addEventListener('submit', function(e) {
+            e.preventDefault();
+            
+            const file = jsonFileInput.files[0];
+            if (!file) {
+                alert('Veuillez sélectionner un fichier JSON.');
+                return;
+            }
+            
+            // Vérifier l'extension du fichier
+            if (!file.name.endsWith('.json')) {
+                alert('Le fichier doit être au format JSON.');
+                return;
+            }
+            
+            // Lire le fichier
+            const reader = new FileReader();
+            reader.onload = function(event) {
+                try {
+                    const json = JSON.parse(event.target.result);
+                    
+                    // Vérifier la structure du fichier
+                    if (!json['@context'] || !json.type || !json.orderedItems) {
+                        alert('Le format du fichier JSON n\'est pas celui attendu pour un export de favoris Mastodon.');
+                        return;
+                    }
+                    
+                    favoritesList = json.orderedItems;
+                    totalItems = favoritesList.length;
+                    
+                    // Afficher un résumé
+                    fileSummary.innerHTML = `
+                        <strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon.
+                    `;
+                    
+                    // Passer à l'étape 2
+                    step1.classList.add('d-none');
+                    step2.classList.remove('d-none');
+                    
+                    // Stocker les données dans localStorage pour les conserver
+                    localStorage.setItem('favmastokey_favorites', JSON.stringify(favoritesList));
+                    
+                } catch (error) {
+                    alert('Erreur lors de l\'analyse du fichier JSON: ' + error.message);
+                }
+            };
+            
+            reader.onerror = function() {
+                alert('Erreur lors de la lecture du fichier.');
+            };
+            
+            reader.readAsText(file);
+        });
+    }
+    
+    // Gestion du retour à l'étape 1
+    if (backToStep1) {
+        backToStep1.addEventListener('click', function() {
+            step2.classList.add('d-none');
+            step1.classList.remove('d-none');
+        });
+    }
+    
+    // Gérer la connexion à Misskey
+    if (misskeyForm) {
+        misskeyForm.addEventListener('submit', function(e) {
+            e.preventDefault();
+            
+            const instanceInput = document.getElementById('misskey-instance');
+            let instance = instanceInput.value.trim();
+            
+            // Vérifier que l'instance est valide
+            if (!instance) {
+                alert('Veuillez entrer l\'URL de votre instance Misskey.');
+                return;
+            }
+            
+            // Supprimer le protocole et les slash de fin si présents
+            instance = instance.replace(/^https?:\/\//, '').replace(/\/$/, '');
+            
+            // Vérifier que l'URL semble valide
+            if (!/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(instance)) {
+                alert('L\'URL de l\'instance Misskey semble invalide.');
+                return;
+            }
+            
+            // Rediriger vers l'authentification OAuth
+            window.location.href = `oauth.php?instance=${encodeURIComponent(instance)}`;
+        });
+    }
+    
+    // Gérer le processus de migration
+    if (startMigration) {
+        startMigration.addEventListener('click', function() {
+            if (isProcessing) return;
+            
+            // Récupérer les favoris depuis localStorage si nécessaire
+            if (favoritesList.length === 0 && localStorage.getItem('favmastokey_favorites')) {
+                favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites'));
+                totalItems = favoritesList.length;
+            }
+            
+            if (favoritesList.length === 0) {
+                addLogEntry('Aucun favori à migrer. Veuillez d\'abord télécharger votre fichier JSON.', 'error');
+                return;
+            }
+            
+            // Démarrer la migration
+            isProcessing = true;
+            isPaused = false;
+            startMigration.classList.add('d-none');
+            pauseMigration.classList.remove('d-none');
+            
+            addLogEntry('Démarrage de la migration...', 'info');
+            
+            // Lancer le processus de migration
+            processBatch();
+        });
+    }
+    
+    // Gérer la pause de la migration
+    if (pauseMigration) {
+        pauseMigration.addEventListener('click', function() {
+            if (!isProcessing) return;
+            
+            isPaused = !isPaused;
+            
+            if (isPaused) {
+                pauseMigration.textContent = 'Reprendre';
+                addLogEntry('Migration en pause.', 'warning');
+                currentProgress.classList.remove('active');
+            } else {
+                pauseMigration.textContent = 'Pause';
+                addLogEntry('Reprise de la migration...', 'info');
+                currentProgress.classList.add('active');
+                processBatch();
+            }
+        });
+    }
+    
+    // Gérer l'annulation de la migration
+    if (cancelMigration) {
+        cancelMigration.addEventListener('click', function() {
+            if (!isProcessing && currentIndex === 0) {
+                // Retour à l'étape 1 si rien n'a commencé
+                step3.classList.add('d-none');
+                step1.classList.remove('d-none');
+                return;
+            }
+            
+            const confirmCancel = confirm('Êtes-vous sûr de vouloir annuler la migration en cours ?');
+            if (confirmCancel) {
+                isProcessing = false;
+                isPaused = false;
+                addLogEntry('Migration annulée.', 'error');
+                
+                // Réinitialiser l'interface
+                startMigration.classList.remove('d-none');
+                pauseMigration.classList.add('d-none');
+                pauseMigration.textContent = 'Pause';
+                currentProgress.classList.remove('active');
+            }
+        });
+    }
+    
+    /**
+     * Traite un lot de favoris
+     */
+    function processBatch() {
+        if (!isProcessing || isPaused || currentIndex >= totalItems) {
+            if (currentIndex >= totalItems) {
+                // Migration terminée
+                isProcessing = false;
+                addLogEntry('Migration terminée avec succès !', 'success');
+                startMigration.classList.remove('d-none');
+                startMigration.textContent = 'Terminer';
+                startMigration.addEventListener('click', function() {
+                    // Nettoyer localStorage et retourner à l'étape 1
+                    localStorage.removeItem('favmastokey_favorites');
+                    step3.classList.add('d-none');
+                    step1.classList.remove('d-none');
+                });
+                pauseMigration.classList.add('d-none');
+                
+                // Mettre à jour la progression à 100%
+                updateProgress(100);
+            }
+            return;
+        }
+        
+        // Nombre d'éléments à traiter dans ce lot
+        const batchSize = 5;
+        const endIndex = Math.min(currentIndex + batchSize, totalItems);
+        
+        // Préparer les éléments du lot
+        const batch = favoritesList.slice(currentIndex, endIndex);
+        
+        // Mettre à jour la progression actuelle
+        currentProgress.classList.add('active');
+        updateProgress();
+        
+        // Simuler le traitement (à remplacer par l'appel API réel)
+        addLogEntry(`Traitement du lot ${currentIndex + 1} à ${endIndex}...`, 'info');
+        
+        // Envoyer la requête au serveur
+        fetch('process.php', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                batch: batch,
+                currentIndex: currentIndex,
+                totalItems: totalItems
+            })
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                // Traiter les résultats
+                if (data.results && data.results.length) {
+                    data.results.forEach(result => {
+                        addLogEntry(result.message, result.status);
+                    });
+                }
+                
+                // Mettre à jour l'index
+                currentIndex = endIndex;
+                
+                // Mettre à jour la progression
+                updateProgress();
+                
+                // Traiter le lot suivant après un court délai
+                setTimeout(processBatch, 1000);
+            } else {
+                // Gérer l'erreur
+                addLogEntry('Erreur: ' + data.message, 'error');
+                
+                // Pause en cas d'erreur
+                isPaused = true;
+                pauseMigration.textContent = 'Reprendre';
+                currentProgress.classList.remove('active');
+            }
+        })
+        .catch(error => {
+            // Gérer l'erreur réseau
+            addLogEntry('Erreur de connexion: ' + error.message, 'error');
+            
+            // Pause en cas d'erreur
+            isPaused = true;
+            pauseMigration.textContent = 'Reprendre';
+            currentProgress.classList.remove('active');
+        });
+    }
+    
+    /**
+     * Met à jour la barre de progression
+     */
+    function updateProgress(forcedValue = null) {
+        const progress = forcedValue !== null ? forcedValue : (currentIndex / totalItems) * 100;
+        globalProgress.style.width = progress + '%';
+        globalProgress.textContent = Math.round(progress) + '%';
+        globalProgress.setAttribute('aria-valuenow', progress);
+        
+        if (forcedValue === null) {
+            // Mettre à jour la progression actuelle
+            const batchProgress = ((currentIndex % 5) / 5) * 100;
+            currentProgress.style.width = batchProgress + '%';
+            currentProgress.setAttribute('aria-valuenow', batchProgress);
+        } else {
+            currentProgress.style.width = '100%';
+            currentProgress.setAttribute('aria-valuenow', 100);
+        }
+    }
+    
+    /**
+     * Ajoute une entrée dans le journal des opérations
+     */
+    function addLogEntry(message, status = 'info') {
+        const entry = document.createElement('div');
+        entry.className = `log-entry ${status}`;
+        
+        const timestamp = new Date().toLocaleTimeString();
+        entry.textContent = `[${timestamp}] ${message}`;
+        
+        operationLog.appendChild(entry);
+        
+        // Scroll vers le bas pour voir la dernière entrée
+        const logContainer = document.getElementById('log-container');
+        logContainer.scrollTop = logContainer.scrollHeight;
+    }
+});
\ No newline at end of file
diff --git a/oauth.php b/oauth.php
new file mode 100644
index 0000000..da6d5d0
--- /dev/null
+++ b/oauth.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * FavMasToKey - Authentification OAuth avec Misskey
+ */
+
+// Définir la constante pour inclure les fichiers
+define('FAVMASTOKEY', true);
+
+// Inclure les fichiers requis
+require_once 'includes/config.php';
+require_once 'includes/functions.php';
+
+// Vérifier si l'instance est fournie
+if (!isset($_GET['instance']) || empty($_GET['instance'])) {
+    $_SESSION['messages'][] = [
+        'type' => 'danger',
+        'text' => 'Aucune instance Misskey spécifiée.'
+    ];
+    header('Location: index.php');
+    exit;
+}
+
+// Récupérer l'instance
+$instance = trim($_GET['instance']);
+
+// Supprimer le protocole et les slash de fin si présents
+$instance = preg_replace('/^https?:\/\//', '', $instance);
+$instance = rtrim($instance, '/');
+
+// Vérifier que l'instance semble valide
+if (!preg_match('/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $instance)) {
+    $_SESSION['messages'][] = [
+        'type' => 'danger',
+        'text' => 'L\'URL de l\'instance Misskey semble invalide.'
+    ];
+    header('Location: index.php');
+    exit;
+}
+
+// Stocker l'instance dans la session
+$_SESSION['misskey_instance'] = $instance;
+
+// En production, ici nous devrions vérifier si l'application est déjà enregistrée sur cette instance
+// Si ce n'est pas le cas, il faudrait l'enregistrer via l'API Misskey
+// Pour cette version initiale, nous utilisons des valeurs simulées
+
+// Générer l'URL d'autorisation
+try {
+    // REMARQUE: Pour une implémentation réelle, $config['client_id'] et $config['client_secret'] 
+    // devraient être stockés par instance car chaque instance Misskey nécessite une application distincte
+    
+    // Générer un état aléatoire pour la sécurité
+    $state = bin2hex(random_bytes(16));
+    $_SESSION['oauth_state'] = $state;
+    
+    // Construire l'URL de callback (doit correspondre à celle configurée dans l'application Misskey)
+    $callback_url = $config['app_url'] . '/callback.php';
+    
+    // Paramètres de la requête d'autorisation
+    $params = [
+        'client_id' => $config['client_id'] ?: 'DEMO_CLIENT_ID', // En production, utiliser une valeur réelle
+        'response_type' => 'code',
+        'redirect_uri' => $callback_url,
+        'scope' => 'write:favorites',
+        'state' => $state
+    ];
+    
+    // Construire l'URL d'autorisation
+    $auth_url = "https://{$instance}/oauth/authorize?" . http_build_query($params);
+    
+    // Rediriger vers l'URL d'autorisation
+    header('Location: ' . $auth_url);
+    exit;
+    
+} catch (Exception $e) {
+    $_SESSION['messages'][] = [
+        'type' => 'danger',
+        'text' => 'Erreur lors de la préparation de l\'authentification: ' . $e->getMessage()
+    ];
+    header('Location: index.php');
+    exit;
+}
\ No newline at end of file
diff --git a/process.php b/process.php
new file mode 100644
index 0000000..c9f88ac
--- /dev/null
+++ b/process.php
@@ -0,0 +1,156 @@
+<?php
+/**
+ * FavMasToKey - Traitement des favoris
+ */
+
+// Définir la constante pour inclure les fichiers
+define('FAVMASTOKEY', true);
+
+// Inclure les fichiers requis
+require_once 'includes/config.php';
+require_once 'includes/functions.php';
+
+// Vérifier que la requête est en POST
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+    http_response_code(405);
+    echo json_encode(['success' => false, 'message' => 'Méthode non autorisée']);
+    exit;
+}
+
+// Vérifier que l'utilisateur est authentifié
+if (!isset($_SESSION['misskey_token']) || empty($_SESSION['misskey_token'])) {
+    http_response_code(401);
+    echo json_encode(['success' => false, 'message' => 'Non authentifié']);
+    exit;
+}
+
+// Récupérer l'instance Misskey
+$misskey_instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : '';
+if (empty($misskey_instance)) {
+    http_response_code(400);
+    echo json_encode(['success' => false, 'message' => 'Instance Misskey non définie']);
+    exit;
+}
+
+// Récupérer le token d'accès
+$token = $_SESSION['misskey_token'];
+
+// Récupérer les données envoyées
+$input = json_decode(file_get_contents('php://input'), true);
+if (!$input || !isset($input['batch']) || !is_array($input['batch'])) {
+    http_response_code(400);
+    echo json_encode(['success' => false, 'message' => 'Données invalides']);
+    exit;
+}
+
+// Récupérer le lot à traiter
+$batch = $input['batch'];
+$currentIndex = isset($input['currentIndex']) ? (int)$input['currentIndex'] : 0;
+$totalItems = isset($input['totalItems']) ? (int)$input['totalItems'] : count($batch);
+
+// Résultats du traitement
+$results = [];
+
+// Traiter chaque URL du lot
+foreach ($batch as $index => $url) {
+    // Extraire les informations de l'URL
+    $urlParts = parse_url($url);
+    
+    // Vérifier que l'URL est valide
+    if (!$urlParts || !isset($urlParts['host']) || !isset($urlParts['path'])) {
+        $results[] = [
+            'status' => 'error',
+            'message' => "URL invalide: $url"
+        ];
+        continue;
+    }
+    
+    // Extraire l'identifiant du toot
+    $pathParts = explode('/', trim($urlParts['path'], '/'));
+    $tootId = end($pathParts);
+    
+    if (!is_numeric($tootId)) {
+        $results[] = [
+            'status' => 'error',
+            'message' => "Impossible d'extraire l'ID du toot: $url"
+        ];
+        continue;
+    }
+    
+    // Construire l'URL pour la recherche fédérée sur Misskey
+    $searchUrl = "https://" . $urlParts['host'] . "/@" . $pathParts[count($pathParts) - 3] . "/" . $tootId;
+    
+    // En production, ici nous ferions une recherche sur Misskey pour trouver l'équivalent du toot
+    // Pour cette version initiale, nous simulons la réussite/échec
+    
+    // Simulation : 90% de réussite, 10% d'échec
+    $success = (rand(1, 10) <= 9);
+    
+    if ($success) {
+        // Simulation de l'ajout aux favoris
+        $results[] = [
+            'status' => 'success',
+            'message' => "Ajouté aux favoris: $url"
+        ];
+    } else {
+        // Simulation d'erreur
+        $results[] = [
+            'status' => 'error',
+            'message' => "Impossible d'ajouter aux favoris: $url (publication introuvable ou inaccessible)"
+        ];
+    }
+    
+    // Dans un cas réel, nous aurions un code similaire à celui-ci:
+    /*
+    // Rechercher la note sur Misskey
+    $searchData = [
+        'query' => $searchUrl,
+        'limit' => 1
+    ];
+    
+    $searchResult = misskey_api_request($misskey_instance, '/api/notes/search', $searchData, $token);
+    
+    if ($searchResult['success'] && !empty($searchResult['data'])) {
+        // Récupérer l'ID de la note trouvée
+        $noteId = $searchResult['data'][0]['id'];
+        
+        // Ajouter aux favoris
+        $favoriteData = [
+            'noteId' => $noteId
+        ];
+        
+        $favoriteResult = misskey_api_request($misskey_instance, '/api/notes/favorites/create', $favoriteData, $token);
+        
+        if ($favoriteResult['success']) {
+            $results[] = [
+                'status' => 'success',
+                'message' => "Ajouté aux favoris: $url"
+            ];
+        } else {
+            $results[] = [
+                'status' => 'error',
+                'message' => "Erreur lors de l'ajout aux favoris: " . $favoriteResult['message']
+            ];
+        }
+    } else {
+        $results[] = [
+            'status' => 'error',
+            'message' => "Publication introuvable sur Misskey: $url"
+        ];
+    }
+    */
+    
+    // Pause pour éviter de surcharger l'API (à utiliser en production)
+    // usleep(200000); // 200 ms
+}
+
+// Renvoyer les résultats
+echo json_encode([
+    'success' => true,
+    'results' => $results,
+    'progress' => [
+        'current' => $currentIndex + count($batch),
+        'total' => $totalItems,
+        'percentage' => round((($currentIndex + count($batch)) / $totalItems) * 100, 2)
+    ]
+]);
\ No newline at end of file