diff --git a/public/css/main.css b/public/css/main.css
index c5f25cb..b51c2f1 100644
--- a/public/css/main.css
+++ b/public/css/main.css
@@ -1,4 +1,63 @@
/* Layout général */
+body {
+ overflow-x: hidden;
+}
+
+/* Sidebar styles */
+.sidebar {
+ width: 250px;
+ min-height: 100vh;
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 100;
+ transition: all 0.3s;
+}
+
+.sidebar-header {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.sidebar .nav-link {
+ padding: 0.75rem 1rem;
+ border-radius: 0.25rem;
+ margin-bottom: 0.25rem;
+ transition: all 0.2s;
+}
+
+.sidebar .nav-link:hover {
+ background-color: rgba(255, 255, 255, 0.1);
+}
+
+.sidebar .nav-link.active {
+ background-color: rgba(255, 255, 255, 0.2);
+ font-weight: bold;
+}
+
+.sidebar .nav-link i {
+ margin-right: 0.5rem;
+}
+
+.content-wrapper {
+ margin-left: 250px;
+ width: calc(100% - 250px);
+ min-height: 100vh;
+}
+
+/* For mobile devices */
+@media (max-width: 768px) {
+ .sidebar {
+ width: 100%;
+ position: relative;
+ min-height: auto;
+ }
+
+ .content-wrapper {
+ margin-left: 0;
+ width: 100%;
+ }
+}
+
.body-landing {
background-color: rgb(255, 255, 255);
min-height: 100vh;
@@ -9,7 +68,7 @@
.main-header {
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
- padding: 1rem 0;
+ /*padding: 1rem 0;*/
margin-bottom: 2rem;
}
diff --git a/src/Controller/WikiController.php b/src/Controller/WikiController.php
index 4cec428..843d794 100644
--- a/src/Controller/WikiController.php
+++ b/src/Controller/WikiController.php
@@ -205,9 +205,16 @@ class WikiController extends AbstractController
if (isset($recentChangesData['recent_changes']) && is_array($recentChangesData['recent_changes'])) {
$recentChanges = $recentChangesData['recent_changes'];
$lastUpdated = isset($recentChangesData['last_updated']) ? $recentChangesData['last_updated'] : null;
-
+
// Process team members statistics
$teamMembers = $this->processTeamMembersStats($recentChanges);
+
+ // Add avatar URLs to recent changes
+ foreach ($recentChanges as &$change) {
+ if (isset($change['user'])) {
+ $change['avatar_url'] = $this->fetchUserAvatar($change['user']);
+ }
+ }
}
// Check if the data is older than 1 hour
@@ -241,6 +248,72 @@ class WikiController extends AbstractController
]);
}
+ /**
+ * Fetch and cache user avatar from OSM Wiki
+ *
+ * @param string $username OSM Wiki username
+ * @return string|null URL of the avatar or null if not found
+ */
+ private function fetchUserAvatar(string $username): ?string
+ {
+ // Create cache directory if it doesn't exist
+ $cacheDir = $this->getParameter('kernel.project_dir') . '/wiki_compare/avatar_cache';
+ if (!file_exists($cacheDir)) {
+ mkdir($cacheDir, 0755, true);
+ }
+
+ // Generate cache filename
+ $cacheFile = $cacheDir . '/' . md5($username) . '.json';
+
+ // Check if avatar is cached and not expired (7 days)
+ if (file_exists($cacheFile)) {
+ $cacheData = json_decode(file_get_contents($cacheFile), true);
+ $cacheTime = new \DateTime($cacheData['timestamp'] ?? '2000-01-01');
+ $now = new \DateTime();
+ $diff = $now->diff($cacheTime);
+
+ // If cache is less than 7 days old, return cached avatar
+ if ($diff->days < 7 && isset($cacheData['avatar_url'])) {
+ return $cacheData['avatar_url'];
+ }
+ }
+
+ // Avatar not cached or cache expired, fetch from wiki
+ $userPageUrl = "https://wiki.openstreetmap.org/wiki/User:" . urlencode($username);
+ try {
+ $response = file_get_contents($userPageUrl);
+ if ($response) {
+ // Parse HTML to find avatar
+ $avatarUrl = null;
+
+ // Look for user avatar in the page
+ if (preg_match('/]*class="[^"]*userImage[^"]*"[^>]*src="([^"]+)"/', $response, $matches)) {
+ $avatarUrl = $matches[1];
+
+ // Make URL absolute if it's relative
+ if (strpos($avatarUrl, 'http') !== 0) {
+ $avatarUrl = 'https://wiki.openstreetmap.org' . $avatarUrl;
+ }
+ }
+
+ // Cache the result
+ $cacheData = [
+ 'username' => $username,
+ 'avatar_url' => $avatarUrl,
+ 'timestamp' => (new \DateTime())->format('c')
+ ];
+ file_put_contents($cacheFile, json_encode($cacheData));
+
+ return $avatarUrl;
+ }
+ } catch (\Exception $e) {
+ // Log error but continue
+ error_log("Error fetching avatar for user $username: " . $e->getMessage());
+ }
+
+ return null;
+ }
+
/**
* Process team members statistics from recent changes data
*
@@ -258,13 +331,17 @@ class WikiController extends AbstractController
// Initialize user data if not exists
if (!isset($teamMembers[$user])) {
+ // Fetch user avatar
+ $avatarUrl = $this->fetchUserAvatar($user);
+
$teamMembers[$user] = [
'username' => $user,
'contributions' => 0,
'chars_added' => 0,
'chars_changed' => 0,
'chars_deleted' => 0,
- 'user_url' => "https://wiki.openstreetmap.org/wiki/User:" . urlencode($user)
+ 'user_url' => "https://wiki.openstreetmap.org/wiki/User:" . urlencode($user),
+ 'avatar_url' => $avatarUrl
];
}
@@ -628,9 +705,18 @@ class WikiController extends AbstractController
// Also load the word-diff based suspicious pages for comparison
if (file_exists($wordDiffFile)) {
- $jsonData = json_decode(file_get_contents($wordDiffFile), true);
-
- foreach ($jsonData as $page) {
+ // Use memory-efficient approach to extract only the necessary data
+ $maxItems = 50; // Limit the number of items to prevent memory exhaustion
+
+ // Extract regular_pages and specific_pages arrays
+ $regularPages = $this->extractJsonArrayByKey($wordDiffFile, 'regular_pages', $maxItems);
+ $specificPages = $this->extractJsonArrayByKey($wordDiffFile, 'specific_pages', $maxItems);
+
+ // Combine them into a single array
+ $allPages = array_merge($regularPages, $specificPages);
+
+ // Process each page to find suspicious deletions
+ foreach ($allPages as $page) {
if (isset($page['fr_page']) && isset($page['en_page'])) {
// Calculate deletion percentage
$enWordCount = (int)$page['en_page']['word_count'];
@@ -641,6 +727,11 @@ class WikiController extends AbstractController
if ($wordDiff > 0 && $frWordCount > 0 && ($wordDiff / $enWordCount) > 0.3) {
$page['deletion_percentage'] = round(($wordDiff / $enWordCount) * 100, 2);
$wordDiffPages[] = $page;
+
+ // Limit the number of suspicious pages to prevent memory issues
+ if (count($wordDiffPages) >= 20) {
+ break;
+ }
}
}
}
@@ -794,8 +885,18 @@ class WikiController extends AbstractController
return $this->redirectToRoute('app_admin_wiki');
}
- // Select a random page from the combined pages
- $randomPage = $allPages[array_rand($allPages)];
+ // Filter pages to ensure they have an en_page key
+ $validPages = array_filter($allPages, function($page) {
+ return isset($page['en_page']) && !empty($page['en_page']);
+ });
+
+ // If no valid pages, use any page but ensure we handle missing en_page in the template
+ if (empty($validPages)) {
+ $randomPage = $allPages[array_rand($allPages)];
+ } else {
+ // Select a random page from the valid pages
+ $randomPage = $validPages[array_rand($validPages)];
+ }
return $this->render('admin/wiki_random_suggestion.html.twig', [
'page' => $randomPage
@@ -902,7 +1003,97 @@ EOT;
]);
}
- #[Route('/wiki/archived-proposals', name: 'app_admin_wiki_archived_proposals')]
+ #[Route('/wiki/dashboard', name: 'app_admin_wiki_dashboard')]
+ public function dashboard(): Response
+ {
+ // Get metrics data from JSON files
+ $metricsData = $this->getDashboardMetrics();
+
+ return $this->render('admin/wiki_dashboard.html.twig', [
+ 'metrics' => $metricsData
+ ]);
+ }
+
+ /**
+ * Get metrics data for the dashboard
+ *
+ * @return array Metrics data
+ */
+ private function getDashboardMetrics(): array
+ {
+ $metrics = [
+ 'average_scores' => [],
+ 'tracked_pages' => [],
+ 'orphaned_pages' => [],
+ 'uncategorized_pages' => [],
+ 'dates' => []
+ ];
+
+ // Get data from outdated_pages.json for average scores and tracked pages
+ $outdatedPagesFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/outdated_pages.json';
+ if (file_exists($outdatedPagesFile)) {
+ $historyEntries = $this->extractJsonArrayByKey($outdatedPagesFile, 'history', 100);
+
+ // Process history entries
+ foreach ($historyEntries as $date => $entry) {
+ if (isset($entry['global_metrics'])) {
+ $globalMetrics = $entry['global_metrics'];
+
+ // Format date for display
+ $formattedDate = (new \DateTime($date))->format('Y-m-d');
+ $metrics['dates'][] = $formattedDate;
+
+ // Get average staleness score
+ $metrics['average_scores'][] = $globalMetrics['avg_staleness'] ?? 0;
+
+ // Get number of tracked pages
+ $totalPages = $globalMetrics['total_pages'] ?? 0;
+ $metrics['tracked_pages'][] = $totalPages;
+ }
+ }
+ }
+
+ // Get data from deadend_pages.json for uncategorized pages
+ $deadendPagesFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/deadend_pages.json';
+ if (file_exists($deadendPagesFile)) {
+ $historyEntries = $this->extractJsonArrayByKey($deadendPagesFile, 'history', 100);
+
+ // Process history entries
+ foreach ($historyEntries as $date => $entry) {
+ // Format date for display
+ $formattedDate = (new \DateTime($date))->format('Y-m-d');
+
+ // If date already exists in metrics, use the same index
+ $dateIndex = array_search($formattedDate, $metrics['dates']);
+ if ($dateIndex === false) {
+ $dateIndex = count($metrics['dates']);
+ $metrics['dates'][] = $formattedDate;
+
+ // Add placeholder values for other metrics if this is a new date
+ if (!isset($metrics['average_scores'][$dateIndex])) {
+ $metrics['average_scores'][$dateIndex] = 0;
+ }
+ if (!isset($metrics['tracked_pages'][$dateIndex])) {
+ $metrics['tracked_pages'][$dateIndex] = 0;
+ }
+ }
+
+ // Get number of uncategorized pages
+ $uncategorizedCount = isset($entry['pages']) ? count($entry['pages']) : 0;
+ $metrics['uncategorized_pages'][$dateIndex] = $uncategorizedCount;
+ }
+ }
+
+ // Sort dates and reindex arrays
+ array_multisort($metrics['dates'], SORT_ASC,
+ $metrics['average_scores'],
+ $metrics['tracked_pages'],
+ $metrics['uncategorized_pages']);
+
+ return $metrics;
+ }
+
+ #[Route('/wiki/archived-proposals', name: 'app_admin_wiki_archived_proposals')]
public function archivedProposals(\Symfony\Component\HttpFoundation\Request $request): Response
{
$jsonFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/archived_proposals.json';
@@ -1233,6 +1424,20 @@ EOT;
$keysWithoutWiki = $keysWithoutWikiData;
}
}
+
+ // Load deadend pages (pages starting with "France" from the DeadendPages list)
+ $deadendPages = [];
+ $categorizedPages = [];
+ $deadendPagesFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/deadend_pages.json';
+ if (file_exists($deadendPagesFile)) {
+ $deadendPagesData = json_decode(file_get_contents($deadendPagesFile), true);
+ if (isset($deadendPagesData['pages']) && is_array($deadendPagesData['pages'])) {
+ $deadendPages = $deadendPagesData['pages'];
+ }
+ if (isset($deadendPagesData['categorized_pages']) && is_array($deadendPagesData['categorized_pages'])) {
+ $categorizedPages = $deadendPagesData['categorized_pages'];
+ }
+ }
return $this->render('admin/wiki.html.twig', [
'wiki_pages' => $wikiPages,
@@ -1244,7 +1449,9 @@ EOT;
'staleness_stats' => $stalenessStats,
'wiki_pages_stats' => $wikiPagesStats,
'available_translations' => $availableTranslations,
- 'keys_without_wiki' => $keysWithoutWiki
+ 'keys_without_wiki' => $keysWithoutWiki,
+ 'deadend_pages' => $deadendPages,
+ 'categorized_pages' => $categorizedPages
]);
}
diff --git a/templates/admin/wiki.html.twig b/templates/admin/wiki.html.twig
index c8fef3a..076b08b 100644
--- a/templates/admin/wiki.html.twig
+++ b/templates/admin/wiki.html.twig
@@ -4,7 +4,6 @@
{% block body %}
Outil de qualité des des pages wiki OpenStreetMap en français et en anglais pour les clés OSM @@ -570,6 +569,101 @@
Ces pages wiki commençant par "France" n'ont pas de catégorie. Vous pouvez contribuer en ajoutant des catégories à ces pages.
+Titre | +Catégories suggérées | +Actions | +
---|---|---|
+ {{ page.title }} + | ++ {% if page.suggested_categories is defined and page.suggested_categories|length > 0 %} + {% for category in page.suggested_categories %} + {{ category }} + {% endfor %} + {% else %} + Aucune suggestion + {% endif %} + | ++ + | +
Ces pages wiki commençant par "France" ont été récemment catégorisées et ne sont plus dans la liste des pages sans catégorie.
+Titre | +Actions | +
---|---|
+
+
+
+ {{ page.title }}
+ Catégorisée
+
+ |
+
+
+
+ Voir
+
+
+ |
+
le score de fraîcheur prend en compte d'avantage la différence entre le nombre de mots que l'ancienneté de modification. diff --git a/templates/admin/wiki_archived_proposals.html.twig b/templates/admin/wiki_archived_proposals.html.twig index 72fbfd2..7b6b1b0 100644 --- a/templates/admin/wiki_archived_proposals.html.twig +++ b/templates/admin/wiki_archived_proposals.html.twig @@ -128,8 +128,7 @@ {% block body %}
Analyse des votes sur les propositions archivées du wiki OSM
@@ -182,7 +181,7 @@Utilisez cette page pour traduire la page wiki en français. La page anglaise est affichée à gauche pour référence, et le formulaire d'édition de la page française est à droite.
diff --git a/templates/admin/wiki_dashboard.html.twig b/templates/admin/wiki_dashboard.html.twig new file mode 100644 index 0000000..2fb7e59 --- /dev/null +++ b/templates/admin/wiki_dashboard.html.twig @@ -0,0 +1,254 @@ +{% extends 'base.html.twig' %} + +{% block title %}Tableau de bord - Wiki OSM{% endblock %} + +{% block body %} +Suivi de l'évolution des métriques du wiki OpenStreetMap
+ ++ {% if metrics.average_scores|length > 0 %} + {{ metrics.average_scores|last|number_format(1) }} + {% else %} + 0 + {% endif %} +
++ {% if metrics.tracked_pages|length > 0 %} + {{ metrics.tracked_pages|last }} + {% else %} + 0 + {% endif %} +
++ {% if metrics.uncategorized_pages|length > 0 %} + {{ metrics.uncategorized_pages|last }} + {% else %} + 0 + {% endif %} +
++ {% if metrics.dates|length > 0 %} + {{ metrics.dates|last }} + {% else %} + - + {% endif %} +
+Date | +Score moyen | +Pages suivies | +Pages sans catégorie | +
---|---|---|---|
{{ metrics.dates[i] }} | +{{ metrics.average_scores[i]|default(0)|number_format(1) }} | +{{ metrics.tracked_pages[i]|default(0) }} | +{{ metrics.uncategorized_pages[i]|default(0) }} | +
diff --git a/templates/admin/wiki_missing_translations.html.twig b/templates/admin/wiki_missing_translations.html.twig index 5698544..b099445 100644 --- a/templates/admin/wiki_missing_translations.html.twig +++ b/templates/admin/wiki_missing_translations.html.twig @@ -4,7 +4,6 @@ {% block body %}
Liste des pages françaises du wiki OSM qui n'ont pas de traduction en anglais.
diff --git a/templates/admin/wiki_osm_fr_groups.html.twig b/templates/admin/wiki_osm_fr_groups.html.twig index b314eb3..0faa2ec 100644 --- a/templates/admin/wiki_osm_fr_groups.html.twig +++ b/templates/admin/wiki_osm_fr_groups.html.twig @@ -4,7 +4,6 @@ {% block body %}Liste des groupes de travail et des groupes locaux d'OpenStreetMap France.
diff --git a/templates/admin/wiki_pages_unavailable_in_french.html.twig b/templates/admin/wiki_pages_unavailable_in_french.html.twig index eb212ff..e1b68ae 100644 --- a/templates/admin/wiki_pages_unavailable_in_french.html.twig +++ b/templates/admin/wiki_pages_unavailable_in_french.html.twig @@ -4,7 +4,6 @@ {% block body %}Liste des pages du wiki OSM qui n'ont pas de traduction française, groupées par langue d'origine.
diff --git a/templates/admin/wiki_random_suggestion.html.twig b/templates/admin/wiki_random_suggestion.html.twig index 2bce17e..7392066 100644 --- a/templates/admin/wiki_random_suggestion.html.twig +++ b/templates/admin/wiki_random_suggestion.html.twig @@ -4,7 +4,6 @@ {% block body %}Voici une page wiki qui a besoin d'être améliorée.
@@ -25,28 +24,34 @@- Dernière modification: {{ page.en_page.last_modified }} + Dernière modification: {{ page.en_page is defined and page.en_page.last_modified is defined ? page.en_page.last_modified : 'Non disponible' }}
Liste des changements récents dans l'espace de noms français du wiki OpenStreetMap.
@@ -26,18 +25,27 @@ {% for member in team_members %}Liste des propositions de tags OpenStreetMap actuellement en cours de vote ou récemment modifiées.
diff --git a/templates/base.html.twig b/templates/base.html.twig index 8dfdb04..a0c4dd6 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -21,97 +21,141 @@ {% endblock %} -Ces pages wiki commençant par "France" n'ont pas de catégorie. Vous pouvez contribuer en ajoutant des catégories à ces pages.
+Ces pages wiki commençant par "France" ont été récemment catégorisées et ne sont plus dans la liste des pages sans catégorie.
+