├── fragments ├── header.php ├── dashboard.php └── item.php ├── lib ├── transition │ ├── dashboard.php │ ├── item.php │ ├── api_dashboard_get.php │ ├── item_clock.php │ ├── item_demo.php │ ├── api_dashboard_store.php │ ├── demo.php │ ├── item_chart.php │ ├── item_rss_clean.php │ ├── item_table.php │ ├── item_demo_table.php │ ├── item_addon_updates.php │ ├── item_backup_status.php │ ├── item_media_storage.php │ ├── item_new_articles.php │ ├── item_system_status.php │ ├── item_user_activity.php │ ├── item_article_status.php │ ├── item_big_number_demo.php │ ├── item_chart_bar.php │ ├── item_chart_pie.php │ ├── item_countdown_demo.php │ ├── item_demo_big_number.php │ ├── item_demo_chart_line.php │ ├── item_demo_chart_pie.php │ ├── item_chart_line.php │ ├── item_recent_articles.php │ ├── item_addon_statistics.php │ ├── item_demo_chart_bar_vertical.php │ ├── item_demo_chart_bar_horizontal.php │ └── chart_colors.php ├── Base │ ├── ChartPie.php │ ├── ChartBar.php │ ├── Table.php │ ├── ChartLine.php │ ├── Chart.php │ └── Item.php ├── DemoItems │ ├── ChartBarVertical.php │ ├── ChartPie.php │ ├── ChartBarHorizontal.php │ ├── Info.php │ ├── Table.php │ ├── ChartLine.php │ ├── DashboardDemo.php │ └── BigNumber.php ├── Api │ ├── Get.php │ └── Store.php ├── traits │ └── ChartColors.php ├── Items │ ├── ArticleStatus.php │ ├── AddonStatistics.php │ ├── UserActivity.php │ ├── RecentArticles.php │ ├── NewArticles.php │ ├── MediaStorage.php │ ├── BackupStatus.php │ ├── SystemStatus.php │ ├── AddonUpdates.php │ ├── BigNumberDemo.php │ ├── CountdownDemo.php │ ├── RssClean.php │ └── Clock.php ├── dashboard.php └── DashboardDefault.php ├── package.yml ├── .github └── workflows │ └── publish-to-redaxo-org.yml ├── LICENSE ├── boot.php ├── lang ├── en_gb.lang └── de_de.lang ├── CHANGELOG.md ├── pages ├── config.php └── index.php ├── assets └── css │ └── dashboard2-style.css └── README.md /fragments/header.php: -------------------------------------------------------------------------------- 1 | getVar('widgetSelect'); 2 | -------------------------------------------------------------------------------- /lib/transition/dashboard.php: -------------------------------------------------------------------------------- 1 | chartType = 'doughnut'; 12 | return $this; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/Base/ChartBar.php: -------------------------------------------------------------------------------- 1 | chartOptions['indexAxis'] = 'y'; 12 | return $this; 13 | } 14 | 15 | public function setVertical() 16 | { 17 | unset($this->chartOptions['orientation']); 18 | return $this; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.yml: -------------------------------------------------------------------------------- 1 | package: dashboard 2 | version: '2.2.1' 3 | author: 'Friends Of REDAXO' 4 | supportpage: https://github.com/FriendsOfRedaxo/dashboard 5 | 6 | page: 7 | title: 'translate:title' 8 | perm: dashboard[] 9 | block: system 10 | prio: 10 11 | pjax: true 12 | icon: rex-icon fas fa-th 13 | 14 | #pages: 15 | # system/log/cronjob: 16 | # title: Cronjob 17 | # perm: admin[] 18 | 19 | requires: 20 | php: '>=7.4' 21 | redaxo: ^5.11.0 22 | -------------------------------------------------------------------------------- /lib/DemoItems/ChartBarVertical.php: -------------------------------------------------------------------------------- 1 | 12, 13 | 'Blau' => 19, 14 | 'Gelb' => 3, 15 | 'Grün' => 5, 16 | 'Lila' => 2, 17 | 'Orange' => 3, 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/DemoItems/ChartPie.php: -------------------------------------------------------------------------------- 1 | 12, 13 | 'Blau' => 19, 14 | 'Gelb' => 3, 15 | 'Grün' => 5, 16 | 'Lila' => 2, 17 | 'Orange' => 3, 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/DemoItems/ChartBarHorizontal.php: -------------------------------------------------------------------------------- 1 | setHorizontal(); 13 | } 14 | 15 | public function getChartData() 16 | { 17 | return [ 18 | 'Rot' => random_int(1, 122), 19 | 'Blau' => random_int(1, 122), 20 | 'Gelb' => random_int(1, 122), 21 | 'Grün' => random_int(1, 122), 22 | 'Lila' => random_int(1, 122), 23 | 'Orange' => random_int(1, 122), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/DemoItems/Info.php: -------------------------------------------------------------------------------- 1 | setQuery(' 16 | SELECT id ID 17 | , label Label 18 | , dbtype `DB-Type` 19 | FROM rex_metainfo_type 20 | ORDER BY id ASC 21 | ')->getArray(); 22 | 23 | if (!empty($tableData)) { 24 | $this->data = $tableData; 25 | $this->header = array_keys($tableData[0]); 26 | } 27 | 28 | return [ 29 | 'data' => $this->data, 30 | 'header' => $this->header, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-redaxo-org.yml: -------------------------------------------------------------------------------- 1 | # Instructions: https://github.com/FriendsOfRedaxo/installer-action/ 2 | 3 | name: Publish to REDAXO.org 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | redaxo_publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - if: hashFiles('composer.json') != '' 15 | uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: "8.2" 18 | - if: hashFiles('composer.json') != '' 19 | uses: ramsey/composer-install@v2 20 | with: 21 | composer-options: "--no-dev" 22 | - uses: FriendsOfRedaxo/installer-action@v1 23 | with: 24 | myredaxo-username: ${{ secrets.MYREDAXO_USERNAME }} 25 | myredaxo-api-key: ${{ secrets.MYREDAXO_API_KEY }} 26 | description: ${{ github.event.release.body }} 27 | version: ${{ github.event.release.tag_name }} 28 | -------------------------------------------------------------------------------- /lib/Api/Get.php: -------------------------------------------------------------------------------- 1 | getId()] = [ 24 | 'content' => $item->getContent(true), 25 | 'date' => $item->getCacheDate()->format(rex_i18n::msg('dashboard_action_refresh_title_dateformat')), 26 | ]; 27 | } 28 | 29 | echo json_encode($result); 30 | exit; 31 | // return new rex_api_result(true, json_encode($result)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/DemoItems/ChartLine.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'Rot' => 12, 14 | 'Blau' => 19, 15 | 'Gelb' => 3, 16 | 'Grün' => 5, 17 | 'Lila' => 2, 18 | 'Orange' => 3, 19 | ], 20 | 'Linie 2' => [ 21 | 'Rot' => 3, 22 | 'Blau' => 5, 23 | 'Gelb' => 8, 24 | 'Grün' => 10, 25 | 'Lila' => 11, 26 | 'Orange' => 11.5, 27 | ], 28 | 'Linie 3' => [ 29 | 'Rot' => 5, 30 | 'Blau' => 13, 31 | 'Gelb' => 16, 32 | 'Grün' => 12, 33 | 'Lila' => 7, 34 | 'Orange' => 2, 35 | ], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Friends Of REDAXO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/Api/Store.php: -------------------------------------------------------------------------------- 1 | getId(), []); 27 | foreach ($data as $id => $itemData) { 28 | if (!Dashboard::itemExists($id)) { 29 | continue; 30 | } 31 | 32 | foreach (Item::ATTRIBUTES as $attribute) { 33 | $storeData[$id][$attribute] = (int) ($itemData[$attribute] ?? 0); 34 | } 35 | } 36 | 37 | rex_config::set('dashboard', 'items_' . $user->getId(), $storeData); 38 | 39 | return new rex_api_result(true, rex_i18n::msg('dashboard_api_store_success')); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/transition/chart_colors.php: -------------------------------------------------------------------------------- 1 | colors = $colors; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/traits/ChartColors.php: -------------------------------------------------------------------------------- 1 | colors = $colors; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /boot.php: -------------------------------------------------------------------------------- 1 | getAssetsUrl('css/style.css')); 33 | rex_view::addCssFile($addon->getAssetsUrl('css/dashboard2-style.css')); 34 | 35 | // Bootstrap Table JS (für Tabellen-Widgets) 36 | rex_view::addJsFile($addon->getAssetsUrl('js/table.min.js')); 37 | rex_view::addJsFile($addon->getAssetsUrl('js/table.locale.min.js')); 38 | 39 | rex_view::addJsFile($addon->getAssetsUrl('js/script.js')); 40 | rex_view::addJsFile($addon->getAssetsUrl('js/chart.min.js')); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /fragments/dashboard.php: -------------------------------------------------------------------------------- 1 | 10 |
11 | 32 | getVar('configButton', '') ?>getVar('widgetSelect') ?> 33 |
34 |
getVar('outputActive') ?>
35 |
getVar('outputInactive') ?>
36 | -------------------------------------------------------------------------------- /lib/Base/Table.php: -------------------------------------------------------------------------------- 1 | 'bootstrap-table', 12 | 'data-toggle' => 'table', 13 | 'data-pagination' => 'true', 14 | 'data-page-size' => '10', 15 | ]; 16 | 17 | protected $header = []; 18 | protected $data = []; 19 | 20 | protected function __construct($id, $name) 21 | { 22 | $this->setTableAttribute('data-locale', 'de-DE'); 23 | static::addJs(rex_addon::get('dashboard')->getAssetsUrl('js/table.min.js'), 'table.js'); 24 | static::addJs(rex_addon::get('dashboard')->getAssetsUrl('js/table.locale.min.js'), 'table.locale.js'); 25 | parent::__construct($id, $name); 26 | } 27 | 28 | abstract protected function getTableData(); 29 | 30 | public function setTableAttribute($key, $value) 31 | { 32 | $this->tableAttributes[$key] = $value; 33 | return $this; 34 | } 35 | 36 | protected function getData() 37 | { 38 | $tableData = $this->getTableData(); 39 | 40 | $header = ''; 41 | foreach ($tableData['header'] as $item) { 42 | $header .= '' . htmlspecialchars($item) . ''; 43 | } 44 | 45 | $body = ''; 46 | foreach ($tableData['data'] as $row) { 47 | $body .= ''; 48 | foreach ($row as $item) { 49 | $body .= '' . htmlspecialchars($item) . ''; 50 | } 51 | $body .= ''; 52 | } 53 | 54 | return 'tableAttributes) . '> 55 | ' . $header . ' 56 | ' . $body . ' 57 | '; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /fragments/item.php: -------------------------------------------------------------------------------- 1 | getVar('item'); 14 | $content = $item->getContent(); 15 | ?> 16 |
getAttributes())?>> 17 |
18 |
19 | getOption('show-header')): ?> 20 |
21 |
getName()) ?>
22 |
23 | isCached()): ?> 24 |
25 | 26 |
27 | hasPerm('dashboard[move-items]')): ?> 28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 | 40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /lib/Base/ChartLine.php: -------------------------------------------------------------------------------- 1 | colors; 17 | foreach ($this->getChartData() as $name => $data) { 18 | $chartData = []; 19 | $labels = []; 20 | foreach ($data as $label => $value) { 21 | if (is_array($value)) { 22 | if (array_key_exists('label', $value) && array_key_exists('value', $value)) { 23 | $label = $value['label']; 24 | $value = $value['value']; 25 | } else { 26 | $label = array_shift($value); 27 | $value = array_shift($value); 28 | } 29 | } 30 | 31 | $labels[] = $label; 32 | $chartData[] = $value; 33 | } 34 | 35 | $color = array_shift($colors); 36 | if (is_array($color)) { 37 | if (isset($color[1])) { 38 | $color = $color[1]; 39 | } else { 40 | $color = $color[0]; 41 | } 42 | } 43 | 44 | $datasets[] = [ 45 | 'label' => $name ?: $this->name, 46 | 'data' => $chartData, 47 | 'borderColor' => $color ?? null, 48 | ]; 49 | } 50 | 51 | return ' 52 | 61 | '; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/DemoItems/DashboardDemo.php: -------------------------------------------------------------------------------- 1 | getConfig('demo_enabled', '0')) { 18 | return; 19 | } 20 | 21 | if (rex::isBackend()) { 22 | // Explizit Chart.js laden für Demo-Charts 23 | rex_view::addJsFile($addon->getAssetsUrl('js/chart.min.js')); 24 | 25 | Dashboard::addItem( 26 | Info::factory('dashboard-demo-1', 'Demo 1'), 27 | ); 28 | 29 | Dashboard::addItem( 30 | Info::factory('dashboard-demo-2', 'Demo 2') 31 | ->setColumns(2), 32 | ); 33 | 34 | Dashboard::addItem( 35 | Info::factory('dashboard-demo-3', 'Demo 3') 36 | ->setColumns(3), 37 | ); 38 | 39 | Dashboard::addItem( 40 | ChartBarHorizontal::factory('dashboard-demo-chart-bar-horizontal', 'Chartdemo Balken horizontal'), 41 | ); 42 | 43 | Dashboard::addItem( 44 | ChartBarVertical::factory('dashboard-demo-chart-bar-vertical', 'Chartdemo Balken vertikal'), 45 | ); 46 | 47 | Dashboard::addItem( 48 | ChartPie::factory('dashboard-demo-chart-pie', 'Chartdemo Kreisdiagramm'), 49 | ); 50 | 51 | Dashboard::addItem( 52 | ChartPie::factory('dashboard-demo-chart-donut', 'Chartdemo Donutdiagramm') 53 | ->setDonut(), 54 | ); 55 | 56 | Dashboard::addItem( 57 | Table::factory('dashboard-demo-table-sql', 'Tabelle (SQL)') 58 | ->setTableAttribute('data-locale', 'de-DE'), 59 | ); 60 | 61 | Dashboard::addItem( 62 | ChartLine::factory('dashboard-demo-chart-line', 'Liniendiagramm'), 63 | ); 64 | 65 | Dashboard::addItem( 66 | BigNumber::factory('dashboard-demo-big-number', 'Big Number Demo') 67 | ->setColumns(1), // Als kleines Widget 68 | ); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/Items/ArticleStatus.php: -------------------------------------------------------------------------------- 1 | setOptions([ 21 | 'plugins' => [ 22 | 'tooltip' => [ 23 | 'callbacks' => [ 24 | 'label' => 'function(context) { return context.label + ": " + context.parsed + " ' . rex_i18n::msg('dashboard_articles', 'Artikel') . '"; }', 25 | ], 26 | ], 27 | ], 28 | 'scales' => [ 29 | 'y' => [ 30 | 'beginAtZero' => true, 31 | 'ticks' => [ 32 | 'stepSize' => 1, 33 | ], 34 | ], 35 | ], 36 | ]); 37 | } 38 | 39 | public function getTitle(): string 40 | { 41 | return rex_i18n::msg('dashboard_article_status_title', 'Artikel-Status Übersicht'); 42 | } 43 | 44 | public function getChartData() 45 | { 46 | $sql = rex_sql::factory(); 47 | 48 | // Artikel nach Status 49 | $query = ' 50 | SELECT 51 | status, 52 | COUNT(*) as count 53 | FROM ' . rex::getTable('article') . ' 54 | GROUP BY status 55 | '; 56 | 57 | $data = $sql->getArray($query); 58 | $chartData = []; 59 | 60 | foreach ($data as $row) { 61 | $statusName = 1 == $row['status'] ? rex_i18n::msg('dashboard_online', 'Online') : rex_i18n::msg('dashboard_offline', 'Offline'); 62 | $chartData[$statusName] = (int) $row['count']; 63 | } 64 | 65 | // Zusätzliche Statistiken 66 | $totalArticles = array_sum($chartData); 67 | 68 | // Artikel nach Template (wenn verfügbar) 69 | $templateQuery = ' 70 | SELECT 71 | t.name as template_name, 72 | COUNT(a.id) as count 73 | FROM ' . rex::getTable('article') . ' a 74 | LEFT JOIN ' . rex::getTable('template') . ' t ON a.template_id = t.id 75 | WHERE a.startarticle = 0 76 | GROUP BY a.template_id, t.name 77 | ORDER BY count DESC 78 | LIMIT 5 79 | '; 80 | 81 | $templateData = $sql->getArray($templateQuery); 82 | 83 | // Hauptstatus-Daten zurückgeben 84 | return $chartData; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lang/en_gb.lang: -------------------------------------------------------------------------------- 1 | dashboard_title = Dashboard 2 | dashboard_action_compact = Auto-arrange 3 | dashboard_action_autosize = Resize 4 | dashboard_action_refresh = Refresh all widgets 5 | dashboard_action_auto_refresh = Auto-refresh 6 | dashboard_action_auto_refresh_start = Start auto-refresh 7 | dashboard_action_auto_refresh_pause = Pause auto-refresh 8 | dashboard_select_widget_title = Select widgets 9 | dashboard_action_refresh_title = Data status: 10 | dashboard_action_refresh_title_dateformat = Y-m-d H:i 11 | dashboard_action_hide_title = hide 12 | 13 | dashboard_api_store_failed_backend = Dashboard can only be saved in the backend. 14 | dashboard_api_store_failed_user = You must be logged in to save the dashboard. 15 | dashboard_api_store_success = Dashboard saved successfully. 16 | 17 | dashboard_api_get_failed_user = Access denied. 18 | 19 | # Config 20 | dashboard_config_title = Dashboard Configuration 21 | dashboard_config_demo_title = Demo Elements 22 | dashboard_config_demo_enabled = Enable demo elements 23 | dashboard_config_demo_disabled = Disabled 24 | dashboard_config_saved = Configuration saved successfully. 25 | 26 | # Default Widgets 27 | dashboard_recent_articles_title = Recently updated articles 28 | dashboard_new_articles_title = New articles (30 days) 29 | dashboard_media_storage_title = Media storage usage 30 | dashboard_article_status_title = Article status overview 31 | dashboard_system_status_title = System Status 32 | dashboard_backup_status_title = Backup Status 33 | dashboard_clock_title = Clock 34 | dashboard_user_activity_title = User Activity (7 days) 35 | dashboard_addon_updates_title = AddOn Management 36 | dashboard_addon_statistics_title = AddOn Statistics 37 | dashboard_rss_feed_title = RSS Feed 38 | dashboard_countdown_demo_title = New Year Countdown 39 | dashboard_big_number_demo_title = Follower Count 40 | dashboard_todo_title = ToDo List 41 | 42 | # Table Headers 43 | dashboard_article = Article 44 | dashboard_category = Category 45 | dashboard_language = Language 46 | dashboard_updated = Updated 47 | dashboard_created = Created 48 | dashboard_by = By 49 | dashboard_no_articles = No articles found 50 | dashboard_no_new_articles = No new articles in the last 30 days 51 | dashboard_no_permission = Access denied 52 | dashboard_no_article_permission = No article permissions 53 | 54 | # Chart Data and Label 55 | dashboard_online = Online 56 | dashboard_offline = Offline 57 | dashboard_articles_edited = Articles edited 58 | dashboard_articles_created = Articles created 59 | dashboard_documents = Documents 60 | dashboard_images = Images 61 | dashboard_videos = Videos 62 | dashboard_audio = Audio 63 | dashboard_archives = Archives 64 | dashboard_web_files = Web Files 65 | dashboard_other = Other 66 | dashboard_no_mediapool = No mediapool available 67 | dashboard_no_media_files = No media files found 68 | dashboard_files = files 69 | dashboard_mb = MB 70 | dashboard_articles = articles 71 | 72 | # AddOn Statistics 73 | dashboard_active_addons = Active AddOns 74 | dashboard_core_addons = Core AddOns 75 | dashboard_updates = Updates 76 | dashboard_total = Total 77 | dashboard_admin_only = Available for administrators only 78 | dashboard_inactive_addons_info = AddOn(s) installed but not activated 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Versipn 2.2.0 - xx.09.2025 5 | ------------------------ 6 | 7 | Major-Relase / Breaking Changes - Umstellung auf Namespace 8 | 9 | - Namespace: 10 | - Durchgängige Nutzung des Namespace `FriendsOfRedaxo/Dashboard` 11 | - In einigen Dateien waren bereits im Namespace `FriendsOfRedaxo`. Schreibweise korrigiert in `FriendsOfRedaxo` 12 | und ggf. fehlende Sub-Namespaces gemäß Verzeichnisstruktur ergänzt. 13 | - Klassennamen geändert: z.B. statt `rex_dashboard` einfach `Dashboard`, denn die Eindeutigkeit im Namen entsteht durcg den Namespace. 14 | - Klassennamen vereinfacht: funktionale Prefixe wie `DashboardItem` sind durch die Namespace-Struktur überflüssig. 15 | - API-Klassen in den Namespace aufgenommen; Registrierung unter dem bisherigen API-Namen via **boot.php**. 16 | - Für eine Übergangszeit sind die alten Klassen weiterhin verfügbar; sie verweisen per Extend lediglich auf die neuen Klassen. 17 | 18 | WiP - to be continued 19 | 20 | 21 | Version 2.0.0 – 29.08.2025 22 | -------------------------- 23 | 🎉 **Major Release - Modernisiertes Dashboard** 24 | 25 | **Neue Features:** 26 | - ✨ Standard-Widgets für REDAXO Core-Funktionen 27 | - 📝 Artikel-Widgets mit Benutzerrechte-Integration 28 | - 📊 System-Status und AddOn-Verwaltung (nur Admins) 29 | - 📁 Medien-Speicherverbrauch nach Dateitypen 30 | - 📡 RSS-Feed Widget mit Paginierung (2 Items/Seite) 31 | - 🛡️ Structure-Permissions für alle Artikel-Widgets 32 | - 🎨 Auto-Refresh beim Dashboard-Load (500ms Verzögerung) 33 | - ⚙️ Zentrale Konfigurationsseite für Administratoren 34 | - 📱 Verbessertes Responsive Design 35 | - 🌐 Multi-Language Support mit dynamischen Spalten 36 | 37 | **Änderungen:** 38 | - 🔄 Demo-Plugin aufgelöst und ins Core integriert 39 | - 🏗️ Widget-IDs standardisiert (`dashboard-default-*` Präfix) 40 | - 🔧 RSS-Feed zentral konfigurierbar statt per Widget 41 | - 📈 Media Storage von Chart auf Tabelle umgestellt 42 | - 🗑️ User Activity Widget deaktiviert (Performance) 43 | 44 | **Sicherheit:** 45 | - 🔒 Strenge Berechtigungsprüfung für Admin-Widgets 46 | - 🛡️ XSS-Schutz durch `rex_escape()` für alle Ausgaben 47 | - 🚫 SQL-Injection-Schutz durch Parameter-Binding 48 | - 👤 User-spezifische Widget-Layouts 49 | 50 | **Entfernt:** 51 | - Demo Plugin komplett entfernt 52 | - Veraltete API-Endpoints bereinigt 53 | - Legacy Chart-Code entfernt 54 | 55 | **Migration:** 56 | - Automatische Übernahme bestehender Widget-Positionen 57 | - Neue Widgets initial deaktiviert 58 | - Konfiguration über Dashboard > Konfiguration erforderlich 59 | 60 | Version 1.2 – 28.08.2025 61 | -------------------------- 62 | - Jetzt FOR-AddOn 63 | - Neu: Auto-Refresh 64 | - Button-Leiste verschoben und dezenter 65 | 66 | Version 1.1 – 27.05.2021 67 | -------------------------- 68 | 69 | - Bugfix: JS Aufrufe waren fehlerhaft 70 | - Bugfix: Cachingaufrufe wurden angepasst 71 | - Etwas Logikänderunge bei den Widgetklassenaufrufen 72 | - Quicknavigation nun parallel nutzbar 73 | 74 | Version 1.0 – 18.05.2021 75 | -------------------------- 76 | 77 | - Initiales Setup 78 | - Pie, Balken Charts Klassen 79 | - Listenklasse 80 | - Auswahl von Widgets 81 | - Grid und Grundgerüst 82 | - Refresh der (aller) Widgetdaten 83 | -------------------------------------------------------------------------------- /lib/Base/Chart.php: -------------------------------------------------------------------------------- 1 | getAssetsUrl('js/chart.min.js'), 'chart.js'); 23 | parent::__construct($id, $name); 24 | } 25 | 26 | abstract protected function getChartData(); 27 | 28 | public function setChartType($type) 29 | { 30 | $this->chartType = $type; 31 | return $this; 32 | } 33 | 34 | public function setOptions($options) 35 | { 36 | $this->chartOptions = $options; 37 | return $this; 38 | } 39 | 40 | public function getData() 41 | { 42 | $chartData = []; 43 | $labels = []; 44 | $backgroundColors = []; 45 | $borderColors = []; 46 | 47 | $colors = $this->colors; 48 | 49 | foreach ($this->getChartData() as $label => $value) { 50 | if (is_array($value)) { 51 | if (array_key_exists('label', $value) && array_key_exists('value', $value)) { 52 | $label = $value['label']; 53 | $value = $value['value']; 54 | } else { 55 | $label = array_shift($value); 56 | $value = array_shift($value); 57 | } 58 | } 59 | 60 | $labels[] = $label; 61 | $chartData[] = $value; 62 | 63 | $color = array_shift($colors); 64 | 65 | if (is_array($color)) { 66 | $backgroundColors[] = $color[0]; 67 | 68 | if (isset($color[1])) { 69 | $borderColors[] = $color[1]; 70 | } 71 | } else { 72 | $backgroundColors[] = $color; 73 | } 74 | } 75 | 76 | $dataset = [ 77 | 'label' => $this->name, 78 | 'data' => $chartData, 79 | 'backgroundColor' => $backgroundColors, 80 | ]; 81 | 82 | if (!empty($borderColors)) { 83 | $dataset['borderColor'] = $borderColors; 84 | $dataset['borderWidth'] = 1; 85 | } 86 | 87 | return ' 88 | 98 | '; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lang/de_de.lang: -------------------------------------------------------------------------------- 1 | dashboard_title = Dashboard 2 | dashboard_action_compact = Automatisch anordnen 3 | dashboard_action_autosize = Größe anpassen 4 | dashboard_action_refresh = Alle Widgets aktualisieren 5 | dashboard_action_auto_refresh = Auto-Aktualisierung 6 | dashboard_action_auto_refresh_start = Auto-Aktualisierung starten 7 | dashboard_action_auto_refresh_pause = Auto-Aktualisierung pausieren 8 | dashboard_select_widget_title = Widgets auswählen 9 | dashboard_action_refresh_title = Datenstand: 10 | dashboard_action_refresh_title_dateformat = d.m.Y H:i 11 | dashboard_action_hide_title = ausblenden 12 | 13 | dashboard_api_store_failed_backend = Das Speichern des Dashboards ist nur im Backend möglich. 14 | dashboard_api_store_failed_user = Zum Speichern des Dashboards muss man angemeldet sein. 15 | dashboard_api_store_success = Speichern des Dashboards erfolgreich. 16 | 17 | dashboard_api_get_failed_user = Keine Berechtigung. 18 | 19 | # Config 20 | dashboard_config_title = Dashboard Konfiguration 21 | dashboard_config_demo_title = Demo-Elemente 22 | dashboard_config_demo_enabled = Demo-Elemente aktivieren 23 | dashboard_config_demo_disabled = Deaktiviert 24 | dashboard_config_saved = Konfiguration erfolgreich gespeichert. 25 | 26 | # Default Widgets 27 | dashboard_recent_articles_title = Zuletzt aktualisierte Artikel 28 | dashboard_new_articles_title = Neue Artikel (30 Tage) 29 | dashboard_media_storage_title = Medien-Speicherverbrauch nach Typ 30 | dashboard_article_status_title = Artikel-Status Übersicht 31 | dashboard_system_status_title = System-Status 32 | dashboard_backup_status_title = Backup-Status 33 | dashboard_clock_title = Uhr 34 | dashboard_user_activity_title = Benutzer-Aktivität (7 Tage) 35 | dashboard_addon_updates_title = AddOn Updates & Übersicht 36 | dashboard_addon_statistics_title = AddOn Statistiken 37 | dashboard_rss_feed_title = RSS-Feed 38 | dashboard_countdown_demo_title = Countdown Neujahr 39 | dashboard_big_number_demo_title = Follower Count 40 | 41 | # Tabellen-Überschriften 42 | dashboard_article = Artikel 43 | dashboard_category = Kategorie 44 | dashboard_language = Sprache 45 | dashboard_updated = Aktualisiert 46 | dashboard_created = Erstellt 47 | dashboard_by = Von 48 | dashboard_no_articles = Keine Artikel gefunden 49 | dashboard_no_new_articles = Keine neuen Artikel in den letzten 30 Tagen 50 | dashboard_no_permission = Keine Berechtigung 51 | dashboard_no_article_permission = Keine Artikel-Berechtigung 52 | 53 | # Chart-Daten und Labels 54 | dashboard_online = Online 55 | dashboard_offline = Offline 56 | dashboard_articles_edited = Artikel bearbeitet 57 | dashboard_articles_created = Artikel erstellt 58 | dashboard_documents = Dokumente 59 | dashboard_images = Bilder 60 | dashboard_videos = Videos 61 | dashboard_audio = Audio 62 | dashboard_archives = Archive 63 | dashboard_web_files = Web-Dateien 64 | dashboard_other = Sonstige 65 | dashboard_no_mediapool = Kein Mediapool verfügbar 66 | dashboard_no_media_files = Keine Mediendateien gefunden 67 | dashboard_files = Dateien 68 | dashboard_mb = MB 69 | dashboard_articles = Artikel 70 | 71 | # AddOn Statistics 72 | dashboard_active_addons = Aktive AddOns 73 | dashboard_core_addons = Core AddOns 74 | dashboard_updates = Updates 75 | dashboard_total = Gesamt 76 | dashboard_admin_only = Nur für Administratoren verfügbar 77 | dashboard_inactive_addons_info = AddOn(s) installiert aber nicht aktiviert 78 | -------------------------------------------------------------------------------- /lib/Items/AddonStatistics.php: -------------------------------------------------------------------------------- 1 | isAdmin()) { 30 | return '

' . rex_i18n::msg('dashboard_admin_only', 'Nur für Administratoren verfügbar.') . '

'; 31 | } 32 | 33 | $content = ''; 34 | 35 | // AddOn-Statistiken sammeln 36 | $allAddons = rex_addon::getAvailableAddons(); 37 | $totalAddons = count($allAddons); 38 | $activeAddons = 0; 39 | $coreAddons = 0; 40 | $availableUpdates = 0; 41 | 42 | foreach ($allAddons as $addon) { 43 | if ($addon->isAvailable()) { 44 | ++$activeAddons; 45 | } 46 | // Zähle Core-AddOns 47 | $coreAddonsList = ['backup', 'be_style', 'cronjob', 'install', 'media_manager', 'mediapool', 'metainfo', 'phpmailer', 'project', 'structure', 'users']; 48 | if (in_array($addon->getPackageId(), $coreAddonsList)) { 49 | ++$coreAddons; 50 | } 51 | } 52 | 53 | // Updates prüfen falls Install-AddOn verfügbar 54 | $installAddon = rex_addon::get('install'); 55 | if ($installAddon->isAvailable()) { 56 | try { 57 | $updatePackages = rex_install_packages::getUpdatePackages(); 58 | $availableUpdates = count($updatePackages); 59 | } catch (Exception $e) { 60 | // Ignoriere Fehler 61 | } 62 | } 63 | 64 | $content .= '
'; 65 | $content .= '
'; 66 | $content .= '

' . $activeAddons . '

'; 67 | $content .= '' . rex_i18n::msg('dashboard_active_addons', 'Aktive AddOns') . ''; 68 | $content .= '
'; 69 | $content .= '
'; 70 | $content .= '

' . $coreAddons . '

'; 71 | $content .= '' . rex_i18n::msg('dashboard_core_addons', 'Core AddOns') . ''; 72 | $content .= '
'; 73 | $content .= '
'; 74 | $content .= '

' . $availableUpdates . '

'; 75 | $content .= '' . rex_i18n::msg('dashboard_updates', 'Updates') . ''; 76 | $content .= '
'; 77 | $content .= '
'; 78 | $content .= '

' . $totalAddons . '

'; 79 | $content .= '' . rex_i18n::msg('dashboard_total', 'Gesamt') . ''; 80 | $content .= '
'; 81 | $content .= '
'; 82 | 83 | // Zusätzliche Details 84 | $inactiveAddons = $totalAddons - $activeAddons; 85 | if ($inactiveAddons > 0) { 86 | $content .= '
'; 87 | $content .= ''; 88 | $content .= ' '; 89 | $content .= $inactiveAddons . ' ' . rex_i18n::msg('dashboard_inactive_addons_info', 'AddOn(s) installiert aber nicht aktiviert'); 90 | $content .= ''; 91 | $content .= '
'; 92 | } 93 | 94 | return $content; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/dashboard.php: -------------------------------------------------------------------------------- 1 | */ 18 | public static $items = []; 19 | 20 | private function __construct() {} 21 | 22 | public static function init() 23 | { 24 | if (null === static::$instance) { 25 | static::$instance = new self(); 26 | } 27 | 28 | foreach (Item::getCssFiles() as $filename) { 29 | rex_view::addCssFile($filename); 30 | } 31 | 32 | foreach (Item::getJsFiles() as $filename) { 33 | rex_view::addJsFile($filename); 34 | } 35 | } 36 | 37 | public static function get() 38 | { 39 | $outputActive = $outputInactive = ''; 40 | foreach (static::$items as $item) { 41 | if ($item->isActive()) { 42 | $outputActive .= (new rex_fragment(['item' => $item]))->parse('item.php'); 43 | } else { 44 | $outputInactive .= (new rex_fragment(['item' => $item]))->parse('item.php'); 45 | } 46 | } 47 | 48 | // Generate widget select for dashboard settings 49 | $select = new rex_select(); 50 | $select->setSize(1); 51 | $select->setName('widgets[]'); 52 | $select->setId('widget-select'); 53 | $select->setMultiple(); 54 | $select->setAttribute('class', 'form-control selectpicker'); 55 | $select->setAttribute('data-selected-text-format', 'static'); 56 | $select->setAttribute('data-title', rex_i18n::msg('dashboard_select_widget_title')); 57 | $select->setAttribute('data-dropdown-align-right', 'auto'); 58 | 59 | foreach (static::$items as $item) { 60 | $select->addOption($item->getName(), $item->getId()); 61 | 62 | if ($item->isActive()) { 63 | $select->setSelected($item->getId()); 64 | } 65 | } 66 | 67 | // Generate config button for admins 68 | $configButton = ''; 69 | if (rex::getUser() && rex::getUser()->isAdmin()) { 70 | $configUrl = rex_url::backendPage('dashboard', ['subpage' => 'config']); 71 | $configButton = ' '; 72 | } 73 | 74 | return (new rex_fragment([ 75 | 'outputActive' => $outputActive, 76 | 'outputInactive' => $outputInactive, 77 | 'widgetSelect' => $select->get(), 78 | 'configButton' => $configButton, 79 | ]))->parse('dashboard.php'); 80 | } 81 | 82 | public static function addItem(Item $item) 83 | { 84 | static::$items[$item->getId()] = $item; 85 | } 86 | 87 | public static function getItem($id): ?Item 88 | { 89 | return static::$items[$id] ?? null; 90 | } 91 | 92 | public static function getItems($ids) 93 | { 94 | $items = []; 95 | 96 | if (empty($ids)) { 97 | return static::$items; 98 | } 99 | 100 | foreach ($ids as $id) { 101 | if (static::itemExists($id)) { 102 | $items[$id] = static::$items[$id]; 103 | } 104 | } 105 | 106 | return $items; 107 | } 108 | 109 | public static function getHeader() 110 | { 111 | return ''; 112 | } 113 | 114 | public static function itemExists($id) 115 | { 116 | foreach (static::$items as $item) { 117 | if ($item->getId() === $id) { 118 | return true; 119 | } 120 | } 121 | 122 | return false; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/Items/UserActivity.php: -------------------------------------------------------------------------------- 1 | setOptions([ 20 | 'plugins' => [ 21 | 'tooltip' => [ 22 | 'callbacks' => [ 23 | 'label' => 'function(context) { return context.dataset.label + ": " + context.parsed.y + " ' . rex_i18n::msg('dashboard_articles', 'Artikel') . '"; }', 24 | ], 25 | ], 26 | ], 27 | 'scales' => [ 28 | 'y' => [ 29 | 'beginAtZero' => true, 30 | 'ticks' => [ 31 | 'stepSize' => 1, 32 | ], 33 | ], 34 | ], 35 | ]); 36 | } 37 | 38 | public function getTitle(): string 39 | { 40 | return rex_i18n::msg('dashboard_user_activity_title', 'Benutzer-Aktivität (7 Tage)'); 41 | } 42 | 43 | public function getChartData() 44 | { 45 | $sql = rex_sql::factory(); 46 | 47 | // Erstelle Datum-Array für die letzten 7 Tage 48 | $chartData = []; 49 | $dates = []; 50 | 51 | for ($i = 6; $i >= 0; --$i) { 52 | $date = date('Y-m-d', strtotime("-{$i} days")); 53 | $dateLabel = date('d.m.', strtotime("-{$i} days")); 54 | $dates[$date] = $dateLabel; 55 | } 56 | 57 | // Artikel-Updates pro Tag 58 | $query = ' 59 | SELECT 60 | DATE(FROM_UNIXTIME(updatedate)) as date, 61 | COUNT(*) as updates 62 | FROM ' . rex::getTable('article') . ' 63 | WHERE updatedate >= ' . strtotime('-7 days') . ' 64 | GROUP BY DATE(FROM_UNIXTIME(updatedate)) 65 | ORDER BY date ASC 66 | '; 67 | 68 | $updateData = $sql->getArray($query); 69 | $updates = []; 70 | 71 | // Initialisiere alle Tage mit 0 72 | foreach ($dates as $date => $label) { 73 | $updates[$label] = 0; 74 | } 75 | 76 | // Fülle tatsächliche Daten 77 | foreach ($updateData as $row) { 78 | if ($row['date']) { // Prüfe auf nicht-null Datum 79 | $dateLabel = date('d.m.', strtotime($row['date'])); 80 | if (isset($updates[$dateLabel])) { 81 | $updates[$dateLabel] = (int) $row['updates']; 82 | } 83 | } 84 | } 85 | 86 | // Neue Artikel pro Tag 87 | $query = ' 88 | SELECT 89 | DATE(FROM_UNIXTIME(createdate)) as date, 90 | COUNT(*) as creates 91 | FROM ' . rex::getTable('article') . ' 92 | WHERE createdate >= ' . strtotime('-7 days') . ' 93 | GROUP BY DATE(FROM_UNIXTIME(createdate)) 94 | ORDER BY date ASC 95 | '; 96 | 97 | $createData = $sql->getArray($query); 98 | $creates = []; 99 | 100 | // Initialisiere alle Tage mit 0 101 | foreach ($dates as $date => $label) { 102 | $creates[$label] = 0; 103 | } 104 | 105 | // Fülle tatsächliche Daten 106 | foreach ($createData as $row) { 107 | if ($row['date']) { // Prüfe auf nicht-null Datum 108 | $dateLabel = date('d.m.', strtotime($row['date'])); 109 | if (isset($creates[$dateLabel])) { 110 | $creates[$dateLabel] = (int) $row['creates']; 111 | } 112 | } 113 | } 114 | 115 | return [ 116 | rex_i18n::msg('dashboard_articles_edited', 'Artikel bearbeitet') => $updates, 117 | rex_i18n::msg('dashboard_articles_created', 'Artikel erstellt') => $creates, 118 | ]; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/Items/RecentArticles.php: -------------------------------------------------------------------------------- 1 | ' . rex_i18n::msg('dashboard_no_permission', 'Keine Berechtigung.') . '

'; 30 | } 31 | 32 | $sql = rex_sql::factory(); 33 | 34 | // Basis-Query 35 | $query = ' 36 | SELECT 37 | a.id, 38 | a.name, 39 | a.updatedate, 40 | a.updateuser, 41 | a.clang_id, 42 | a.parent_id, 43 | c.name as category_name, 44 | cl.name as lang_name 45 | FROM ' . rex::getTable('article') . ' a 46 | LEFT JOIN ' . rex::getTable('article') . ' c ON a.parent_id = c.id AND c.startarticle = 1 AND c.clang_id = a.clang_id 47 | LEFT JOIN ' . rex::getTable('clang') . ' cl ON a.clang_id = cl.id 48 | WHERE a.updatedate > 0'; 49 | 50 | // Benutzerrechte prüfen - nur Artikel anzeigen, auf die der User Zugriff hat 51 | if (!$user->isAdmin() && $user->getComplexPerm('structure')) { 52 | $allowedCategories = $user->getComplexPerm('structure')->getMountpoints(); 53 | if (!empty($allowedCategories)) { 54 | $query .= ' AND a.parent_id IN (' . implode(',', array_map('intval', $allowedCategories)) . ')'; 55 | } else { 56 | // Keine Berechtigung für Kategorien 57 | return '

' . rex_i18n::msg('dashboard_no_article_permission', 'Keine Artikel-Berechtigung.') . '

'; 58 | } 59 | } 60 | 61 | $query .= ' ORDER BY a.updatedate DESC LIMIT 10'; 62 | 63 | $articles = $sql->getArray($query); 64 | 65 | if (empty($articles)) { 66 | return '

' . rex_i18n::msg('dashboard_no_articles', 'Keine Artikel gefunden.') . '

'; 67 | } 68 | 69 | $content = '
'; 70 | $content .= ''; 71 | $content .= ''; 72 | $content .= ''; 73 | $content .= ''; 74 | $content .= ''; 75 | 76 | // Sprach-Spalte nur anzeigen wenn mehrere Sprachen existieren 77 | if (count(rex_clang::getAll()) > 1) { 78 | $content .= ''; 79 | } 80 | 81 | $content .= ''; 82 | $content .= ''; 83 | $content .= ''; 84 | $content .= ''; 85 | $content .= ''; 86 | 87 | foreach ($articles as $article) { 88 | // Prüfe Berechtigung für diesen spezifischen Artikel 89 | if (!$user->isAdmin() && !$user->getComplexPerm('structure')->hasCategoryPerm($article['parent_id'])) { 90 | continue; 91 | } 92 | 93 | $editUrl = rex_url::backendPage('content/edit', [ 94 | 'article_id' => $article['id'], 95 | 'clang' => $article['clang_id'] ?? 1, 96 | ]); 97 | 98 | $content .= ''; 99 | $content .= ''; 100 | $content .= ''; 101 | 102 | // Sprach-Spalte nur anzeigen wenn mehrere Sprachen existieren 103 | if (count(rex_clang::getAll()) > 1) { 104 | $content .= ''; 105 | } 106 | 107 | $content .= ''; 108 | $content .= ''; 109 | $content .= ''; 110 | } 111 | 112 | $content .= ''; 113 | $content .= '
' . rex_i18n::msg('dashboard_article', 'Artikel') . '' . rex_i18n::msg('dashboard_category', 'Kategorie') . '' . rex_i18n::msg('dashboard_language', 'Sprache') . '' . rex_i18n::msg('dashboard_updated', 'Aktualisiert') . '' . rex_i18n::msg('dashboard_by', 'Von') . '
' . rex_escape($article['name']) . '' . rex_escape($article['category_name'] ?: '-') . '' . rex_escape($article['lang_name'] ?: '-') . '' . rex_formatter::strftime($article['updatedate'], 'datetime') . '' . rex_escape($article['updateuser'] ?: '-') . '
'; 114 | $content .= '
'; 115 | 116 | return $content; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/Items/NewArticles.php: -------------------------------------------------------------------------------- 1 | ' . rex_i18n::msg('dashboard_no_permission', 'Keine Berechtigung.') . '

'; 30 | } 31 | 32 | $sql = rex_sql::factory(); 33 | 34 | // Basis-Query 35 | $query = ' 36 | SELECT 37 | a.id, 38 | a.name, 39 | a.createdate, 40 | a.createuser, 41 | a.clang_id, 42 | a.parent_id, 43 | c.name as category_name, 44 | cl.name as lang_name 45 | FROM ' . rex::getTable('article') . ' a 46 | LEFT JOIN ' . rex::getTable('article') . ' c ON a.parent_id = c.id AND c.startarticle = 1 AND c.clang_id = a.clang_id 47 | LEFT JOIN ' . rex::getTable('clang') . ' cl ON a.clang_id = cl.id 48 | WHERE a.createdate > ' . (time() - 30 * 24 * 60 * 60); 49 | 50 | // Benutzerrechte prüfen - nur Artikel anzeigen, auf die der User Zugriff hat 51 | if (!$user->isAdmin() && $user->getComplexPerm('structure')) { 52 | $allowedCategories = $user->getComplexPerm('structure')->getMountpoints(); 53 | if (!empty($allowedCategories)) { 54 | $query .= ' AND a.parent_id IN (' . implode(',', array_map('intval', $allowedCategories)) . ')'; 55 | } else { 56 | // Keine Berechtigung für Kategorien 57 | return '

' . rex_i18n::msg('dashboard_no_article_permission', 'Keine Artikel-Berechtigung.') . '

'; 58 | } 59 | } 60 | 61 | $query .= ' ORDER BY a.createdate DESC LIMIT 15'; 62 | 63 | $articles = $sql->getArray($query); 64 | 65 | if (empty($articles)) { 66 | return '

' . rex_i18n::msg('dashboard_no_new_articles', 'Keine neuen Artikel in den letzten 30 Tagen.') . '

'; 67 | } 68 | 69 | $content = '
'; 70 | $content .= ''; 71 | $content .= ''; 72 | $content .= ''; 73 | $content .= ''; 74 | $content .= ''; 75 | 76 | // Sprach-Spalte nur anzeigen wenn mehrere Sprachen existieren 77 | if (count(rex_clang::getAll()) > 1) { 78 | $content .= ''; 79 | } 80 | 81 | $content .= ''; 82 | $content .= ''; 83 | $content .= ''; 84 | $content .= ''; 85 | $content .= ''; 86 | 87 | foreach ($articles as $article) { 88 | // Prüfe Berechtigung für diesen spezifischen Artikel 89 | if (!$user->isAdmin() && !$user->getComplexPerm('structure')->hasCategoryPerm($article['parent_id'])) { 90 | continue; 91 | } 92 | 93 | $editUrl = rex_url::backendPage('content/edit', [ 94 | 'article_id' => $article['id'], 95 | 'clang' => $article['clang_id'] ?? 1, 96 | ]); 97 | 98 | $content .= ''; 99 | $content .= ''; 100 | $content .= ''; 101 | 102 | // Sprach-Spalte nur anzeigen wenn mehrere Sprachen existieren 103 | if (count(rex_clang::getAll()) > 1) { 104 | $content .= ''; 105 | } 106 | 107 | $content .= ''; 108 | $content .= ''; 109 | $content .= ''; 110 | } 111 | 112 | $content .= ''; 113 | $content .= '
' . rex_i18n::msg('dashboard_article', 'Artikel') . '' . rex_i18n::msg('dashboard_category', 'Kategorie') . '' . rex_i18n::msg('dashboard_language', 'Sprache') . '' . rex_i18n::msg('dashboard_created', 'Erstellt') . '' . rex_i18n::msg('dashboard_by', 'Von') . '
' . rex_escape($article['name']) . '' . rex_escape($article['category_name'] ?: '-') . '' . rex_escape($article['lang_name'] ?: '-') . '' . rex_formatter::strftime($article['createdate'], 'datetime') . '' . rex_escape($article['createuser'] ?: '-') . '
'; 114 | $content .= '
'; 115 | 116 | return $content; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/Items/MediaStorage.php: -------------------------------------------------------------------------------- 1 | setOptions([ 24 | 'plugins' => [ 25 | 'tooltip' => [ 26 | 'callbacks' => [ 27 | 'label' => 'function(context) { return context.label + ": " + context.parsed + " ' . rex_i18n::msg('dashboard_mb', 'MB') . '"; }', 28 | ], 29 | ], 30 | ], 31 | ]); 32 | } 33 | 34 | public function getTitle(): string 35 | { 36 | return rex_i18n::msg('dashboard_media_storage_title', 'Medien-Speicherverbrauch nach Kategorie'); 37 | } 38 | 39 | public function getChartData() 40 | { 41 | $sql = rex_sql::factory(); 42 | 43 | // Prüfe ob Mediapool-Addon aktiv ist 44 | if (!rex_addon::get('mediapool')->isAvailable()) { 45 | return [rex_i18n::msg('dashboard_no_mediapool', 'Kein Mediapool verfügbar') => 1]; 46 | } 47 | 48 | // Definiere Kategorien basierend auf Dateitypen 49 | $categories = [ 50 | rex_i18n::msg('dashboard_documents', 'Dokumente') => ['pdf', 'doc', 'docx', 'txt', 'rtf', 'odt', 'xls', 'xlsx', 'ppt', 'pptx'], 51 | rex_i18n::msg('dashboard_images', 'Bilder') => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'tiff', 'ico'], 52 | rex_i18n::msg('dashboard_videos', 'Videos') => ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'm4v'], 53 | rex_i18n::msg('dashboard_audio', 'Audio') => ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'], 54 | rex_i18n::msg('dashboard_archives', 'Archive') => ['zip', 'rar', '7z', 'tar', 'gz', 'bz2'], 55 | rex_i18n::msg('dashboard_web_files', 'Web-Dateien') => ['css', 'js', 'html', 'htm', 'xml', 'json'], 56 | ]; 57 | 58 | $query = ' 59 | SELECT 60 | LOWER(SUBSTRING_INDEX(filename, ".", -1)) as extension, 61 | SUM(filesize) as total_size, 62 | COUNT(*) as file_count 63 | FROM ' . rex::getTable('media') . ' 64 | WHERE filesize > 0 65 | GROUP BY extension 66 | ORDER BY total_size DESC 67 | '; 68 | 69 | $data = $sql->getArray($query); 70 | $categoryData = []; 71 | $otherSize = 0; 72 | $otherCount = 0; 73 | 74 | if (empty($data)) { 75 | return [rex_i18n::msg('dashboard_no_media_files', 'Keine Mediendateien gefunden') => 1]; 76 | } 77 | 78 | // Initialisiere Kategorien 79 | foreach ($categories as $categoryName => $extensions) { 80 | $categoryData[$categoryName] = ['size' => 0, 'count' => 0]; 81 | } 82 | 83 | // Gruppiere Dateien nach Kategorien 84 | foreach ($data as $row) { 85 | $extension = strtolower($row['extension'] ?: ''); 86 | $size = $row['total_size']; 87 | $count = $row['file_count']; 88 | 89 | $assigned = false; 90 | foreach ($categories as $categoryName => $extensions) { 91 | if (in_array($extension, $extensions)) { 92 | $categoryData[$categoryName]['size'] += $size; 93 | $categoryData[$categoryName]['count'] += $count; 94 | $assigned = true; 95 | break; 96 | } 97 | } 98 | 99 | // Wenn keine Kategorie passt, zu "Sonstige" hinzufügen 100 | if (!$assigned) { 101 | $otherSize += $size; 102 | $otherCount += $count; 103 | } 104 | } 105 | 106 | // Sonstige hinzufügen falls vorhanden 107 | if ($otherSize > 0) { 108 | $categoryData[rex_i18n::msg('dashboard_other', 'Sonstige')] = ['size' => $otherSize, 'count' => $otherCount]; 109 | } 110 | 111 | // Erstelle Chart-Daten 112 | $chartData = []; 113 | foreach ($categoryData as $categoryName => $info) { 114 | if ($info['size'] > 0) { 115 | $sizeInMB = round($info['size'] / (1024 * 1024), 2); 116 | $label = $categoryName . ' (' . $info['count'] . ' ' . rex_i18n::msg('dashboard_files', 'Dateien') . ')'; 117 | $chartData[$label] = $sizeInMB; 118 | } 119 | } 120 | 121 | // Sortiere nach Größe 122 | arsort($chartData); 123 | 124 | return empty($chartData) ? [rex_i18n::msg('dashboard_no_media_files', 'Keine Mediendateien gefunden') => 1] : $chartData; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/Items/BackupStatus.php: -------------------------------------------------------------------------------- 1 | isAdmin(); 29 | } 30 | 31 | public function getData() 32 | { 33 | // Prüfen ob Backup-Addon verfügbar ist 34 | $backupAddon = rex_addon::get('backup'); 35 | if (!$backupAddon->isAvailable()) { 36 | return '
37 | 38 | Backup-Addon ist nicht verfügbar 39 |
'; 40 | } 41 | 42 | try { 43 | // Backup-Informationen sammeln 44 | $backupDir = $backupAddon->getDataPath(); 45 | $sqlBackups = $this->getBackupFiles($backupDir, '*.sql'); 46 | $fileBackups = $this->getBackupFiles($backupDir, '*.tar.gz'); 47 | 48 | $content = '
'; 49 | 50 | // SQL Backup Info 51 | $content .= '
'; 52 | $content .= '
SQL-Backup
'; 53 | $content .= ''; 54 | 55 | if (!empty($sqlBackups)) { 56 | $lastSqlBackup = reset($sqlBackups); 57 | $content .= ''; 58 | $content .= ''; 59 | $content .= ''; 60 | } else { 61 | $content .= ''; 62 | } 63 | 64 | $content .= '
Letztes Backup:' . date('d.m.Y H:i', $lastSqlBackup['created']) . '
Größe:' . $this->formatFileSize($lastSqlBackup['size']) . '
Anzahl Backups:' . count($sqlBackups) . '
Keine SQL-Backups vorhanden
'; 65 | $content .= '
'; 66 | 67 | // Datei Backup Info 68 | $content .= '
'; 69 | $content .= '
Datei-Backup
'; 70 | $content .= ''; 71 | 72 | if (!empty($fileBackups)) { 73 | $lastFileBackup = reset($fileBackups); 74 | $content .= ''; 75 | $content .= ''; 76 | $content .= ''; 77 | } else { 78 | $content .= ''; 79 | } 80 | 81 | $content .= '
Letztes Backup:' . date('d.m.Y H:i', $lastFileBackup['created']) . '
Größe:' . $this->formatFileSize($lastFileBackup['size']) . '
Anzahl Backups:' . count($fileBackups) . '
Keine Datei-Backups vorhanden
'; 82 | $content .= '
'; 83 | 84 | $content .= '
'; 85 | 86 | return $content; 87 | } catch (Exception $e) { 88 | return '
89 | 90 | Fehler beim Laden der Backup-Informationen: ' . rex_escape($e->getMessage()) . ' 91 |
'; 92 | } 93 | } 94 | 95 | private function getBackupFiles($backupDir, $pattern): array 96 | { 97 | $backups = []; 98 | 99 | if (is_dir($backupDir)) { 100 | $files = glob($backupDir . $pattern); 101 | 102 | foreach ($files as $file) { 103 | if (is_file($file)) { 104 | $backups[] = [ 105 | 'filename' => basename($file), 106 | 'size' => function_exists('filesize') ? filesize($file) : 0, 107 | 'created' => function_exists('filemtime') ? filemtime($file) : 0, 108 | ]; 109 | } 110 | } 111 | 112 | // Nach Erstellungsdatum sortieren (neueste zuerst) 113 | usort($backups, static function ($a, $b) { 114 | return $b['created'] - $a['created']; 115 | }); 116 | } 117 | 118 | return $backups; 119 | } 120 | 121 | private function formatFileSize($size): string 122 | { 123 | $units = ['B', 'KB', 'MB', 'GB']; 124 | 125 | for ($i = 0; $size > 1024 && $i < count($units) - 1; ++$i) { 126 | $size /= 1024; 127 | } 128 | 129 | return round($size, 1) . ' ' . $units[$i]; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/Items/SystemStatus.php: -------------------------------------------------------------------------------- 1 | formatBytes(disk_free_space(rex_path::base())); 38 | } else { 39 | $diskFreeSpace = rex_i18n::msg('dashboard_function_disabled', 'Funktion deaktiviert'); 40 | } 41 | 42 | if (function_exists('disk_total_space')) { 43 | $diskTotalSpace = $this->formatBytes(disk_total_space(rex_path::base())); 44 | } else { 45 | $diskTotalSpace = rex_i18n::msg('dashboard_function_disabled', 'Funktion deaktiviert'); 46 | } 47 | 48 | // Cache-Informationen 49 | $cacheDir = rex_path::cache(); 50 | $cacheSize = $this->getDirSize($cacheDir); 51 | 52 | // Aktuelle Benutzer (wenn Backend-Sessions vorhanden) 53 | $currentUsers = $this->getCurrentUsers(); 54 | 55 | $content = '
'; 56 | 57 | // System Info 58 | $content .= '
'; 59 | $content .= '
System
'; 60 | $content .= ''; 61 | $content .= ''; 62 | $content .= ''; 63 | $content .= ''; 64 | $content .= ''; 65 | $content .= '
REDAXO Version:' . $redaxoVersion . '
PHP Version:' . $phpVersion . '
Memory Limit:' . $memoryLimit . '
Max Execution Time:' . $maxExecutionTime . 's
'; 66 | $content .= '
'; 67 | 68 | // Speicher Info 69 | $content .= '
'; 70 | $content .= '
Speicher
'; 71 | $content .= ''; 72 | $content .= ''; 73 | $content .= ''; 74 | $content .= ''; 75 | $content .= '
Freier Speicher:' . $diskFreeSpace . '
Gesamtspeicher:' . $diskTotalSpace . '
Cache-Größe:' . $this->formatBytes($cacheSize) . '
'; 76 | $content .= '
'; 77 | 78 | $content .= '
'; 79 | 80 | // Benutzer-Info 81 | if (!empty($currentUsers)) { 82 | $content .= '
'; 83 | $content .= '
'; 84 | $content .= '
Aktuelle Benutzer (' . count($currentUsers) . ')
'; 85 | $content .= '
'; 86 | 87 | foreach ($currentUsers as $user) { 88 | $content .= '
'; 89 | $content .= '
'; 90 | $content .= '
'; 91 | $content .= '' . rex_escape($user) . ''; 92 | $content .= '
'; 93 | } 94 | 95 | $content .= '
'; 96 | } 97 | 98 | return $content; 99 | } 100 | 101 | private function formatBytes($size, $precision = 2) 102 | { 103 | $units = ['B', 'KB', 'MB', 'GB', 'TB']; 104 | 105 | for ($i = 0; $size > 1024 && $i < count($units) - 1; ++$i) { 106 | $size /= 1024; 107 | } 108 | 109 | return round($size, $precision) . ' ' . $units[$i]; 110 | } 111 | 112 | private function getDirSize($dir) 113 | { 114 | $size = 0; 115 | if (is_dir($dir)) { 116 | $files = new RecursiveIteratorIterator( 117 | new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), 118 | ); 119 | 120 | foreach ($files as $file) { 121 | $size += $file->getSize(); 122 | } 123 | } 124 | 125 | return $size; 126 | } 127 | 128 | private function getCurrentUsers() 129 | { 130 | // Vereinfachte Implementierung - könnte erweitert werden 131 | // mit Session-Tracking oder Login-Log 132 | $user = rex::getUser(); 133 | return $user ? [$user->getLogin()] : []; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/Items/AddonUpdates.php: -------------------------------------------------------------------------------- 1 | isAdmin()) { 30 | return '

Nur für Administratoren verfügbar.

'; 31 | } 32 | 33 | $content = ''; 34 | 35 | // Prüfe ob Install-AddOn verfügbar ist 36 | $installAddon = rex_addon::get('install'); 37 | if (!$installAddon->isAvailable()) { 38 | $content .= '
'; 39 | $content .= ' Install-AddOn nicht verfügbar'; 40 | $content .= '
'; 41 | return $content; 42 | } 43 | 44 | $availableUpdates = []; 45 | $newestAddons = []; 46 | 47 | try { 48 | // Verfügbare Updates abrufen 49 | $updatePackages = rex_install_packages::getUpdatePackages(); 50 | foreach ($updatePackages as $key => $package) { 51 | if (isset($package['files']) && !empty($package['files'])) { 52 | $latestVersion = reset($package['files'])['version']; 53 | $availableUpdates[] = [ 54 | 'name' => $package['name'], 55 | 'key' => $key, 56 | 'current_version' => rex_addon::get($key)->getVersion(), 57 | 'latest_version' => $latestVersion, 58 | ]; 59 | } 60 | } 61 | 62 | // Alle verfügbaren AddOns für "Neueste" Liste (nur 3 Items) 63 | $allPackages = rex_install_packages::getAddPackages(); 64 | 65 | // Sortiere nach Datum der letzten Aktualisierung 66 | uasort($allPackages, static function ($a, $b) { 67 | return strtotime($b['updated']) - strtotime($a['updated']); 68 | }); 69 | 70 | // Nehme die ersten 3 71 | $count = 0; 72 | foreach ($allPackages as $key => $package) { 73 | if ($count >= 3) { 74 | break; 75 | } 76 | 77 | $newestAddons[] = [ 78 | 'name' => $package['name'], 79 | 'author' => $package['author'] ?? '', 80 | 'updated' => $package['updated'], 81 | 'installed' => rex_addon::exists($key), 82 | ]; 83 | ++$count; 84 | } 85 | } catch (Exception $e) { 86 | $content .= '
'; 87 | $content .= ' Fehler beim Abrufen der Daten'; 88 | $content .= '
'; 89 | return $content; 90 | } 91 | 92 | // Update-Status kompakt anzeigen 93 | if (!empty($availableUpdates)) { 94 | $content .= '
'; 95 | $content .= ' ' . count($availableUpdates) . ' Updates verfügbar'; 96 | 97 | foreach (array_slice($availableUpdates, 0, 2) as $addon) { 98 | $content .= '
' . rex_escape($addon['name']) . ' (' . rex_escape($addon['current_version']) . ' → ' . rex_escape($addon['latest_version']) . ')'; 99 | } 100 | 101 | if (count($availableUpdates) > 2) { 102 | $content .= '
... und ' . (count($availableUpdates) - 2) . ' weitere'; 103 | } 104 | 105 | $content .= '
'; 106 | } else { 107 | $content .= '
'; 108 | $content .= ' Alle AddOns aktuell'; 109 | $content .= '
'; 110 | } 111 | 112 | // Neueste AddOns kompakt 113 | if (!empty($newestAddons)) { 114 | $content .= '
Neueste AddOns
'; 115 | 116 | foreach ($newestAddons as $addon) { 117 | $content .= '
'; 118 | $content .= '
'; 119 | $content .= '
'; 120 | $content .= '' . rex_escape($addon['name']) . ''; 121 | if ($addon['installed']) { 122 | $content .= ' Installiert'; 123 | } 124 | $content .= '
von ' . rex_escape($addon['author']) . ''; 125 | $content .= '
'; 126 | $content .= '' . rex_escape(date('d.m.Y', strtotime($addon['updated']))) . ''; 127 | $content .= '
'; 128 | $content .= '
'; 129 | } 130 | } 131 | 132 | return $content; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /pages/config.php: -------------------------------------------------------------------------------- 1 | i18n('config_title')); 6 | 7 | // Initialisiere Default-Werte falls noch nicht gesetzt 8 | if (null === $addon->getConfig('demo_enabled')) { 9 | $addon->setConfig('demo_enabled', '0'); 10 | } 11 | 12 | // Default Widget Defaults initialisieren 13 | if (null === $addon->getConfig('default_widgets_enabled')) { 14 | $addon->setConfig('default_widgets_enabled', '0'); 15 | } 16 | 17 | // Zurück zum Dashboard Button (oberhalb des Formulars, rechts ausgerichtet) 18 | echo '
'; 19 | echo '' . rex_i18n::msg('back') . ' zum Dashboard'; 20 | echo '
'; 21 | 22 | // Instanzieren des Formulars 23 | $form = rex_config_form::factory('dashboard'); 24 | 25 | // Füge subpage Parameter hinzu, damit Form-Action korrekt ist 26 | $form->addParam('subpage', 'config'); 27 | 28 | // Demo aktivieren/deaktivieren - Select 29 | $field = $form->addSelectField('demo_enabled', null, ['class' => 'form-control selectpicker']); 30 | $field->setLabel($addon->i18n('config_demo_enabled')); 31 | $select = $field->getSelect(); 32 | $select->addOption($addon->i18n('config_demo_disabled'), '0'); 33 | $select->addOption($addon->i18n('config_demo_enabled'), '1'); 34 | 35 | // Separator 36 | $form->addRawField('

Default Widgets

'); 37 | 38 | // Default Widgets aktivieren/deaktivieren 39 | $field = $form->addSelectField('default_widgets_enabled', null, ['class' => 'form-control selectpicker']); 40 | $field->setLabel('Default Widgets aktivieren'); 41 | $field->setNotice('Aktiviert nützliche Standard-Widgets für REDAXO Core-Funktionen'); 42 | $select = $field->getSelect(); 43 | $select->addOption('Deaktiviert', '0'); 44 | $select->addOption('Aktiviert', '1'); 45 | 46 | // Nur zeigen wenn Default Widgets aktiviert sind 47 | echo ''; 95 | 96 | // JavaScript für Show/Hide der Widget-Optionen und Size-Controls 97 | echo ''; 145 | 146 | // Ausgabe des Formulars 147 | $fragment = new rex_fragment(); 148 | $fragment->setVar('class', 'edit', false); 149 | $fragment->setVar('title', 'Dashboard Konfiguration', false); 150 | $fragment->setVar('body', $form->get(), false); 151 | echo $fragment->parse('core/page/section.php'); 152 | -------------------------------------------------------------------------------- /lib/Base/Item.php: -------------------------------------------------------------------------------- 1 | 'gs-w', 25 | 'height' => 'gs-h', 26 | 'x' => 'gs-x', 27 | 'y' => 'gs-y', 28 | 'active' => 'data-active', 29 | ]; 30 | 31 | private static $ids = []; 32 | private static $itemData; 33 | private static $jsFiles = []; 34 | private static $cssFiles = []; 35 | 36 | protected $name; 37 | protected $id; 38 | protected $content = ''; 39 | protected $options = [ 40 | 'show-header' => true, 41 | ]; 42 | protected $attributes = [ 43 | 'gs-w' => 1, 44 | 'gs-h' => 3, 45 | 'gs-no-resize' => 1, 46 | ]; 47 | protected $useCache = true; 48 | 49 | protected function __construct($id, $name) 50 | { 51 | $this->id = rex_string::normalize($id); 52 | 53 | if (in_array($this->id, self::$ids)) { 54 | throw new Exception('ID "' . $id . '" (normalized: "' . $this->id . '") is already in use.'); 55 | } 56 | 57 | self::$ids[] = $this->id; 58 | 59 | $this->name = $name; 60 | 61 | /** get stored positions and dimensions of item @see Api/Store */ 62 | if ($user = rex::getUser()) { 63 | if (null === self::$itemData) { 64 | self::$itemData = rex_config::get('dashboard', 'items_' . $user->getId(), []); 65 | } 66 | 67 | if (array_key_exists($this->id, self::$itemData)) { 68 | foreach (static::ATTRIBUTES as $attribute) { 69 | if (array_key_exists($attribute, self::$itemData[$this->id])) { 70 | $this->setAttribute($attribute, self::$itemData[$this->id][$attribute]); 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | abstract protected function getData(); 78 | 79 | public static function factory($id, $name): static 80 | { 81 | $class = self::getFactoryClass(); 82 | return new $class($id, $name); 83 | } 84 | 85 | public function getName() 86 | { 87 | return $this->name; 88 | } 89 | 90 | public function getId() 91 | { 92 | return $this->id; 93 | } 94 | 95 | public function setColumns(int $colCount) 96 | { 97 | $this->attributes['gs-w'] = max(0, min(3, $colCount)); 98 | return $this; 99 | } 100 | 101 | public function getContent($refresh = false) 102 | { 103 | if ($this->useCache) { 104 | $cacheFile = rex_addon::get('dashboard')->getCachePath($this->getId() . '.data'); 105 | if (file_exists($cacheFile) && !$refresh) { 106 | return rex_file::getCache($cacheFile); 107 | } 108 | 109 | $data = $this->getData(); 110 | rex_file::putCache($cacheFile, $data); 111 | return $data; 112 | } 113 | 114 | return $this->getData(); 115 | } 116 | 117 | public function setOption($name, $value) 118 | { 119 | $this->options[$name] = $value; 120 | return $this; 121 | } 122 | 123 | public function getOption($name) 124 | { 125 | return $this->options[$name] ?? null; 126 | } 127 | 128 | public function setAttribute($name, $value) 129 | { 130 | $this->attributes[$name] = $value; 131 | return $this; 132 | } 133 | 134 | public function getAttribute($name) 135 | { 136 | return $this->attributes[$name] ?? null; 137 | } 138 | 139 | public function getAttributes() 140 | { 141 | $this->attributes['data-id'] = $this->getId(); 142 | return $this->attributes; 143 | } 144 | 145 | public function addJs($filename, $name = null) 146 | { 147 | if (file_exists($filename)) { 148 | if (null === $name) { 149 | $name = basename($filename); 150 | } 151 | 152 | if (!array_key_exists($name, self::$jsFiles)) { 153 | self::$jsFiles[$name] = $filename; 154 | } 155 | } 156 | 157 | return $this; 158 | } 159 | 160 | public function isActive($userId = null) 161 | { 162 | if (null === $userId) { 163 | if ($user = rex::getUser()) { 164 | $userId = $user->getId(); 165 | } 166 | } elseif ($userId instanceof rex_user) { 167 | $userId = $userId->getId(); 168 | } else { 169 | $userId = (int) $userId; 170 | } 171 | 172 | if (empty($userId)) { 173 | return false; 174 | } 175 | 176 | return (bool) (self::$itemData[$this->id]['data-active'] ?? false); 177 | } 178 | 179 | public static function getJsFiles() 180 | { 181 | return self::$jsFiles; 182 | } 183 | 184 | public function addCss($filename, $name = null) 185 | { 186 | if (file_exists($filename)) { 187 | if (null === $name) { 188 | $name = basename($filename); 189 | } 190 | 191 | if (!array_key_exists($name, self::$cssFiles)) { 192 | self::$cssFiles[$name] = $filename; 193 | } 194 | } 195 | 196 | return $this; 197 | } 198 | 199 | public static function getCssFiles() 200 | { 201 | return self::$cssFiles; 202 | } 203 | 204 | public function useCache($useCache = true) 205 | { 206 | $this->useCache = $useCache; 207 | return $this; 208 | } 209 | 210 | public function isCached() 211 | { 212 | return $this->useCache; 213 | } 214 | 215 | public function getCacheDate() 216 | { 217 | if (file_exists($cacheFile = rex_addon::get('dashboard')->getCachePath($this->getId() . '.data'))) { 218 | if (function_exists('filemtime')) { 219 | $datetime = new DateTime(); 220 | return $datetime->setTimestamp(filemtime($cacheFile)); 221 | } 222 | } 223 | 224 | return new DateTime(); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /lib/Items/BigNumberDemo.php: -------------------------------------------------------------------------------- 1 | '; 26 | 27 | // Haupt-Zahl (responsive skalierend) 28 | $output .= '
'; 29 | $output .= '
' . number_format($bigNumber, 0, ',', '.') . '
'; 30 | $output .= '
'; 31 | 32 | // Label und Trend 33 | $output .= '
'; 34 | $output .= '
'; 35 | $output .= ' ' . $label; 36 | $output .= '
'; 37 | 38 | if ($trend) { 39 | $trendClass = str_starts_with($trend, '+') ? 'trend-up' : 'trend-down'; 40 | $trendIcon = str_starts_with($trend, '+') ? 'fa-arrow-up' : 'fa-arrow-down'; 41 | $output .= '
'; 42 | $output .= ' ' . $trend; 43 | $output .= '
'; 44 | } 45 | 46 | $output .= '
'; // big-number-info 47 | $output .= ''; // big-number-widget 48 | 49 | // Responsive CSS - angepasst für Dashboard-Umgebung 50 | $output .= ''; 193 | 194 | return $output; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/DemoItems/BigNumber.php: -------------------------------------------------------------------------------- 1 | '; 26 | 27 | // Haupt-Zahl (responsive skalierend) 28 | $output .= '
'; 29 | $output .= '
' . number_format($bigNumber, 0, ',', '.') . '
'; 30 | $output .= '
'; 31 | 32 | // Label und Trend 33 | $output .= '
'; 34 | $output .= '
'; 35 | $output .= ' ' . $label; 36 | $output .= '
'; 37 | 38 | if ($trend) { 39 | $trendClass = str_starts_with($trend, '+') ? 'trend-up' : 'trend-down'; 40 | $trendIcon = str_starts_with($trend, '+') ? 'fa-arrow-up' : 'fa-arrow-down'; 41 | $output .= '
'; 42 | $output .= ' ' . $trend; 43 | $output .= '
'; 44 | } 45 | 46 | $output .= '
'; // big-number-info 47 | $output .= ''; // big-number-widget 48 | 49 | // Responsive CSS 50 | $output .= ''; 179 | 180 | // Optional: JavaScript für animierte Zahl-Updates 181 | $output .= ''; 202 | 203 | return $output; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /pages/index.php: -------------------------------------------------------------------------------- 1 | $feedUrl, 32 | 'name' => $feedName ?: 'RSS Feed', 33 | ]; 34 | 35 | echo json_encode(['success' => true]); 36 | } catch (Exception $e) { 37 | echo json_encode([ 38 | 'success' => false, 39 | 'error' => $e->getMessage(), 40 | ]); 41 | } 42 | exit; 43 | } 44 | 45 | if ('load_rss_feed' === rex_request('ajax', 'string')) { 46 | header('Content-Type: application/json'); 47 | 48 | try { 49 | $widgetId = rex_request('widget_id', 'string'); 50 | $page = (int) rex_request('page', 'int', 1); 51 | 52 | if (empty($widgetId)) { 53 | throw new Exception('Widget ID fehlt'); 54 | } 55 | 56 | // Hole Konfiguration aus Session 57 | if (!isset($_SESSION['dashboard_rss_feeds'][$widgetId])) { 58 | throw new Exception('Feed-Konfiguration nicht gefunden'); 59 | } 60 | 61 | $config = $_SESSION['dashboard_rss_feeds'][$widgetId]; 62 | 63 | // Lade RSS Feed Content 64 | $content = loadRSSFeedContent($config['url'], $config['name'], $page, $widgetId); 65 | 66 | echo json_encode([ 67 | 'success' => true, 68 | 'content' => $content, 69 | ]); 70 | } catch (Exception $e) { 71 | echo json_encode([ 72 | 'success' => false, 73 | 'error' => $e->getMessage(), 74 | ]); 75 | } 76 | exit; 77 | } 78 | 79 | /** 80 | * Lädt RSS Feed Content. 81 | */ 82 | function loadRSSFeedContent(string $feedUrl, string $feedName, int $page = 1, string $widgetId = ''): string 83 | { 84 | $itemsPerPage = 5; 85 | $maxItems = 100; 86 | 87 | // RSS Feed laden 88 | $ch = curl_init(); 89 | curl_setopt($ch, CURLOPT_URL, $feedUrl); 90 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 91 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 92 | curl_setopt($ch, CURLOPT_TIMEOUT, 30); 93 | curl_setopt($ch, CURLOPT_USERAGENT, 'REDAXO Dashboard RSS Reader'); 94 | 95 | $rssData = curl_exec($ch); 96 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 97 | 98 | if (curl_errno($ch) || 200 !== $httpCode) { 99 | curl_close($ch); 100 | throw new Exception('Fehler beim Laden des RSS-Feeds'); 101 | } 102 | 103 | curl_close($ch); 104 | 105 | // XML parsen 106 | libxml_use_internal_errors(true); 107 | $xml = simplexml_load_string($rssData); 108 | 109 | if (!$xml) { 110 | throw new Exception('Ungültiges RSS-Feed-Format'); 111 | } 112 | 113 | $items = []; 114 | 115 | // RSS 2.0 Format 116 | if (isset($xml->channel->item)) { 117 | foreach ($xml->channel->item as $item) { 118 | $items[] = [ 119 | 'title' => (string) $item->title, 120 | 'link' => (string) $item->link, 121 | 'description' => (string) $item->description, 122 | 'pubDate' => (string) $item->pubDate, 123 | ]; 124 | 125 | if (count($items) >= $maxItems) { 126 | break; 127 | } 128 | } 129 | } 130 | // Atom Format 131 | elseif (isset($xml->entry)) { 132 | foreach ($xml->entry as $entry) { 133 | $items[] = [ 134 | 'title' => (string) $entry->title, 135 | 'link' => (string) $entry->link['href'], 136 | 'description' => (string) $entry->summary, 137 | 'pubDate' => (string) $entry->published, 138 | ]; 139 | 140 | if (count($items) >= $maxItems) { 141 | break; 142 | } 143 | } 144 | } 145 | 146 | if (empty($items)) { 147 | return '
Keine RSS-Einträge gefunden.
'; 148 | } 149 | 150 | // Paginierung 151 | $totalItems = count($items); 152 | $totalPages = ceil($totalItems / $itemsPerPage); 153 | $page = max(1, min($page, $totalPages)); 154 | $offset = ($page - 1) * $itemsPerPage; 155 | $pageItems = array_slice($items, $offset, $itemsPerPage); 156 | 157 | // Content generieren 158 | $content = '
'; 159 | 160 | foreach ($pageItems as $item) { 161 | $content .= '
'; 162 | $content .= '
'; 163 | $content .= rex_escape($item['title']) . '
'; 164 | 165 | if ($item['pubDate']) { 166 | $date = date('d.m.Y H:i', strtotime($item['pubDate'])); 167 | $content .= '' . $date . ''; 168 | } 169 | 170 | if ($item['description']) { 171 | $description = strip_tags($item['description']); 172 | $description = mb_strlen($description) > 200 ? mb_substr($description, 0, 200) . '...' : $description; 173 | $content .= '

' . rex_escape($description) . '

'; 174 | } 175 | 176 | $content .= '
'; 177 | } 178 | 179 | // Paginierung 180 | if ($totalPages > 1) { 181 | $content .= '
'; 182 | $content .= ''; 192 | $content .= '
'; 193 | } 194 | 195 | $content .= '
'; 196 | 197 | return $content; 198 | } 199 | 200 | // Handle config subpage 201 | $subpage = rex_request('subpage', 'string', ''); 202 | 203 | if ('config' === $subpage) { 204 | // Check if user is admin 205 | if (!rex::getUser() || !rex::getUser()->isAdmin()) { 206 | throw new rex_exception('Access denied'); 207 | } 208 | 209 | include __DIR__ . '/config.php'; 210 | return; 211 | } 212 | 213 | // Default dashboard view 214 | echo rex_view::title(Dashboard::getHeader() . $addon->i18n('title')); 215 | echo '
' . Dashboard::get() . '
'; 216 | -------------------------------------------------------------------------------- /lib/Items/CountdownDemo.php: -------------------------------------------------------------------------------- 1 | '; 26 | 27 | // Kompakter Header 28 | $output .= '
'; 29 | $output .= '
'; 30 | $output .= ' Neujahr ' . $nextYear; 31 | $output .= '
'; 32 | $output .= '
'; 33 | 34 | // Kompakter Countdown in 2x2 Grid 35 | $output .= '
'; 36 | $output .= '
'; 37 | 38 | // Tage (oben links) 39 | $output .= '
'; 40 | $output .= '
'; 41 | $output .= '
0
'; 42 | $output .= '
Tage
'; 43 | $output .= '
'; 44 | $output .= '
'; 45 | 46 | // Stunden (oben rechts) 47 | $output .= '
'; 48 | $output .= '
'; 49 | $output .= '
0
'; 50 | $output .= '
Std
'; 51 | $output .= '
'; 52 | $output .= '
'; 53 | 54 | // Minuten (unten links) 55 | $output .= '
'; 56 | $output .= '
'; 57 | $output .= '
0
'; 58 | $output .= '
Min
'; 59 | $output .= '
'; 60 | $output .= '
'; 61 | 62 | // Sekunden (unten rechts) 63 | $output .= '
'; 64 | $output .= '
'; 65 | $output .= '
0
'; 66 | $output .= '
Sek
'; 67 | $output .= '
'; 68 | $output .= '
'; 69 | 70 | $output .= '
'; // countdown-grid 71 | $output .= '
'; // countdown-compact 72 | 73 | // Zieldatum für JavaScript 74 | $output .= '
'; 75 | $output .= ''; 76 | $output .= ' ' . date('d.m.Y', $targetDate); 77 | $output .= ''; 78 | $output .= '
'; 79 | 80 | $output .= ''; // countdown-demo-compact 81 | 82 | // JavaScript für Countdown (kompakt) 83 | $output .= ''; 127 | 128 | // Kompaktes CSS 129 | $output .= ''; 191 | 192 | return $output; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /lib/DashboardDefault.php: -------------------------------------------------------------------------------- 1 | getConfig('default_widgets_enabled', false)) { 36 | return; 37 | } 38 | 39 | // Zuletzt aktualisierte Artikel 40 | if ($addon->getConfig('default_recent_articles', true)) { 41 | Dashboard::addItem( 42 | RecentArticles::factory('dashboard-default-recent-articles', rex_i18n::msg('dashboard_recent_articles_title', 'Zuletzt aktualisierte Artikel')) 43 | ->setColumns($addon->getConfig('default_recent_articles_columns', 2)), 44 | ); 45 | } 46 | 47 | // Neue Artikel 48 | if ($addon->getConfig('default_new_articles', true)) { 49 | Dashboard::addItem( 50 | NewArticles::factory('dashboard-default-new-articles', rex_i18n::msg('dashboard_new_articles_title', 'Neue Artikel (30 Tage)')) 51 | ->setColumns($addon->getConfig('default_new_articles_columns', 2)), 52 | ); 53 | } 54 | 55 | // Medien-Speicherverbrauch (Chart) 56 | if ($addon->getConfig('default_media_storage', true)) { 57 | Dashboard::addItem( 58 | MediaStorage::factory('dashboard-default-media-storage', rex_i18n::msg('dashboard_media_storage_title', 'Medien-Speicherverbrauch')) 59 | ->setColumns($addon->getConfig('default_media_storage_columns', 1)), 60 | ); 61 | } 62 | 63 | // Artikel-Status Übersicht (Chart) 64 | if ($addon->getConfig('default_article_status', true)) { 65 | Dashboard::addItem( 66 | ArticleStatus::factory('dashboard-default-article-status', rex_i18n::msg('dashboard_article_status_title', 'Artikel-Status')) 67 | ->setColumns($addon->getConfig('default_article_status_columns', 1)), 68 | ); 69 | } 70 | 71 | // System-Status (nur für Admins) 72 | if ($addon->getConfig('default_system_status', true) && rex::getUser() && rex::getUser()->isAdmin()) { 73 | Dashboard::addItem( 74 | SystemStatus::factory('dashboard-default-system-status', rex_i18n::msg('dashboard_system_status_title', 'System-Status')) 75 | ->setColumns($addon->getConfig('default_system_status_columns', 2)), 76 | ); 77 | } 78 | 79 | // Backup-Status (nur für Admins) 80 | if ($addon->getConfig('default_backup_status', true) && rex::getUser() && rex::getUser()->isAdmin()) { 81 | Dashboard::addItem( 82 | BackupStatus::factory('dashboard-default-backup-status', rex_i18n::msg('dashboard_backup_status_title', 'Backup-Status')) 83 | ->setColumns($addon->getConfig('default_backup_status_columns', 1)), 84 | ); 85 | } 86 | 87 | // Uhr Widget 88 | if ($addon->getConfig('default_clock', true)) { 89 | Dashboard::addItem( 90 | Clock::factory('dashboard-default-clock', rex_i18n::msg('dashboard_clock_title', 'Uhr')) 91 | ->setColumns($addon->getConfig('default_clock_columns', 1)), 92 | ); 93 | } 94 | 95 | // AddOn Updates (nur für Admins) 96 | if ($addon->getConfig('default_addon_updates', true) && rex::getUser() && rex::getUser()->isAdmin()) { 97 | Dashboard::addItem( 98 | AddonUpdates::factory('dashboard-default-addon-updates', rex_i18n::msg('dashboard_addon_updates_title', 'AddOn Updates & Übersicht')) 99 | ->setColumns($addon->getConfig('default_addon_updates_columns', 2)), 100 | ); 101 | } 102 | 103 | // AddOn Statistiken (nur für Admins) 104 | if ($addon->getConfig('default_addon_statistics', true) && rex::getUser() && rex::getUser()->isAdmin()) { 105 | Dashboard::addItem( 106 | AddonStatistics::factory('dashboard-default-addon-statistics', rex_i18n::msg('dashboard_addon_statistics_title', 'AddOn Statistiken')) 107 | ->setColumns($addon->getConfig('default_addon_statistics_columns', 1)), 108 | ); 109 | } 110 | 111 | // Benutzer-Aktivität (Chart) (nur für Admins) 112 | if ($addon->getConfig('default_user_activity', true) && rex::getUser() && rex::getUser()->isAdmin()) { 113 | Dashboard::addItem( 114 | UserActivity::factory('dashboard-default-user-activity', rex_i18n::msg('dashboard_user_activity_title', 'Benutzer-Aktivität (Chart)')) 115 | ->setColumns($addon->getConfig('default_user_activity_columns', 1)), 116 | ); 117 | } 118 | 119 | // RSS-Feed Widget (Clean Version ohne Bootstrap Table) 120 | Dashboard::addItem( 121 | RssClean::factory('dashboard-default-rss-feed', rex_i18n::msg('dashboard_rss_feed_title', 'RSS-Feed')) 122 | ->setColumns($addon->getConfig('default_rss_feed_columns', 2)), 123 | ); 124 | 125 | // Demo Countdown Widget (nur wenn Demo-Items erlaubt) 126 | if ($addon->getConfig('demo_items_enabled', false) && $addon->getConfig('default_countdown_demo', true)) { 127 | Dashboard::addItem( 128 | CountdownDemo::factory('dashboard-default-countdown-demo', rex_i18n::msg('dashboard_countdown_demo_title', 'Countdown Neujahr')) 129 | ->setColumns($addon->getConfig('default_countdown_demo_columns', 1)), 130 | ); 131 | } 132 | 133 | // Big Number Demo Widget (nur wenn Demo-Items erlaubt) 134 | if ($addon->getConfig('demo_items_enabled', false) && $addon->getConfig('default_big_number_demo', true)) { 135 | Dashboard::addItem( 136 | BigNumberDemo::factory('dashboard-default-big-number-demo', rex_i18n::msg('dashboard_big_number_demo_title', 'Follower Count')) 137 | ->setColumns($addon->getConfig('default_big_number_demo_columns', 1)), 138 | ); 139 | } 140 | } 141 | 142 | /** 143 | * Initialisiert die Default-Konfiguration. 144 | */ 145 | public static function initDefaults() 146 | { 147 | $addon = rex_addon::get('dashboard'); 148 | 149 | // Setze Default-Werte für alle Widgets 150 | $defaults = [ 151 | // Debug-Modus temporär aktiviert 152 | 'debug' => false, 153 | 'default_widgets_enabled' => false, // Muss explizit aktiviert werden 154 | 'demo_items_enabled' => false, // Demo-Items müssen explizit aktiviert werden 155 | 'default_recent_articles' => true, 156 | 'default_recent_articles_columns' => 2, // normal breit (2 Spalten) 157 | 'default_new_articles' => true, 158 | 'default_new_articles_columns' => 2, // normal breit (2 Spalten) 159 | 'default_media_storage' => true, 160 | 'default_media_storage_columns' => 1, // klein 161 | 'default_article_status' => true, 162 | 'default_article_status_columns' => 1, // klein 163 | 'default_system_status' => true, 164 | 'default_system_status_columns' => 2, // breit 165 | 'default_backup_status' => true, 166 | 'default_backup_status_columns' => 1, // klein 167 | 'default_clock' => true, 168 | 'default_clock_columns' => 1, // klein 169 | 'default_addon_updates' => true, 170 | 'default_addon_updates_columns' => 2, // breit 171 | 'default_addon_statistics' => true, 172 | 'default_addon_statistics_columns' => 1, // klein 173 | 'default_user_activity' => true, 174 | 'default_user_activity_columns' => 1, // klein 175 | 'default_rss_feed' => true, 176 | 'default_rss_feed_columns' => 2, // breit 177 | 'default_countdown_demo' => true, 178 | 'default_countdown_demo_columns' => 1, // klein (1 Spalte) 179 | 'default_big_number_demo' => true, 180 | 'default_big_number_demo_columns' => 1, // klein (1 Spalte) 181 | 'rss_feed_url' => '', 182 | 'rss_items_per_page' => 2, 183 | // RSS-Feeds Konfiguration - KEINE Standard-URLs setzen! 184 | 'rss_feeds' => [ 185 | // User soll eigene RSS-Feeds konfigurieren 186 | ], 187 | ]; 188 | 189 | foreach ($defaults as $key => $defaultValue) { 190 | if (null === $addon->getConfig($key)) { 191 | $addon->setConfig($key, $defaultValue); 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/Items/RssClean.php: -------------------------------------------------------------------------------- 1 | !empty($legacyName) ? $legacyName : 'RSS-Feed', 43 | 'url' => $legacyUrl, 44 | ], 45 | ]; 46 | } 47 | } 48 | 49 | if (empty($rssFeeds)) { 50 | return '
51 | 52 | Keine RSS-Feeds konfiguriert
53 | Gehe zu Dashboard > Konfiguration um RSS-Feeds hinzuzufügen. 54 |
'; 55 | } 56 | 57 | // Ersten konfigurierten Feed verwenden 58 | $firstFeed = reset($rssFeeds); 59 | $rssUrl = $firstFeed['url'] ?? ''; 60 | $feedTitle = $firstFeed['title'] ?? 'RSS-Feed'; 61 | 62 | if (empty($rssUrl)) { 63 | return '
64 | 65 | RSS-Feed URL ist leer
66 | Bitte prüfe die Konfiguration. 67 |
'; 68 | } 69 | 70 | // RSS-Feed laden 71 | $rssData = $this->loadRssFeed($rssUrl); 72 | 73 | if (!$rssData) { 74 | return '
75 | 76 | RSS-Feed konnte nicht geladen werden
77 | URL: ' . rex_escape($rssUrl) . '
78 | Mögliche Ursachen: SSL-Probleme, Timeout, ungültiges XML 79 |
'; 80 | } 81 | 82 | if (empty($rssData['items'])) { 83 | return '
84 | 85 | RSS-Feed ist leer
86 | Feed: ' . rex_escape($feedTitle) . ' 87 |
'; 88 | } 89 | 90 | // JavaScript-basierte Paginierung - alle Items laden, per JS 2 pro Seite anzeigen 91 | $itemsPerPage = 2; 92 | $maxItems = min(30, count($rssData['items'])); // Maximal 30 Items laden 93 | $items = array_slice($rssData['items'], 0, $maxItems); 94 | $totalPages = ceil($maxItems / $itemsPerPage); 95 | 96 | $output = '
'; 97 | $output .= '
' . rex_escape($feedTitle) . '
'; 98 | 99 | // Container für RSS-Items 100 | $output .= '
'; 101 | 102 | // Alle RSS-Items als HTML generieren 103 | foreach ($items as $index => $item) { 104 | $pubDate = !empty($item['pubDate']) ? date('d.m.Y H:i', strtotime($item['pubDate'])) : ''; 105 | $page = floor($index / $itemsPerPage) + 1; 106 | $isFirstPage = 1 === $page; 107 | 108 | $output .= '
'; 109 | $output .= '
'; 110 | $output .= ''; 111 | $output .= rex_escape($item['title']); 112 | $output .= ' '; 113 | $output .= '
'; 114 | 115 | if (!empty($item['description'])) { 116 | $description = strip_tags($item['description']); 117 | $description = mb_substr($description, 0, 120) . (mb_strlen($description) > 120 ? '...' : ''); 118 | $output .= '

' . rex_escape($description) . '

'; 119 | } 120 | 121 | if ($pubDate) { 122 | $output .= ' ' . $pubDate . ''; 123 | } 124 | 125 | $output .= '
'; 126 | 127 | // Trennlinie zwischen Items der gleichen Seite 128 | $isLastItemOnPage = ($index + 1) % $itemsPerPage === 0; 129 | $isLastItemOverall = $index === count($items) - 1; 130 | 131 | if (!$isLastItemOnPage && !$isLastItemOverall) { 132 | $output .= '
'; 133 | } 134 | } 135 | 136 | $output .= '
'; // Ende rss-items-container 137 | 138 | // JavaScript-Paginierung für Bootstrap 3 139 | if ($totalPages > 1) { 140 | $output .= ''; 163 | } 164 | 165 | // JavaScript für Paginierung 166 | $output .= ' 167 | '; 246 | 247 | $output .= '
'; 248 | 249 | return $output; 250 | } 251 | 252 | /** 253 | * Lädt RSS-Feed Daten mit verbesserter Fehlerbehandlung. 254 | */ 255 | private function loadRssFeed($url) 256 | { 257 | try { 258 | // Timeout und User-Agent setzen 259 | $context = stream_context_create([ 260 | 'http' => [ 261 | 'timeout' => 15, 262 | 'user_agent' => 'REDAXO Dashboard Widget 2.0', 263 | 'method' => 'GET', 264 | 'header' => "Accept: application/rss+xml, application/xml, text/xml\r\n", 265 | ], 266 | // SSL-Verifizierung aktiviert lassen für Sicherheit 267 | /* 268 | 'ssl' => [ 269 | 'verify_peer' => false, 270 | 'verify_peer_name' => false, 271 | 'allow_self_signed' => true, 272 | ], 273 | */ 274 | ]); 275 | 276 | // RSS-Feed laden 277 | $xml = @file_get_contents($url, false, $context); 278 | 279 | if (false === $xml) { 280 | $error = error_get_last(); 281 | error_log('RSS Feed Error: Could not load URL ' . $url . ' - ' . ($error['message'] ?? 'Unknown error')); 282 | return false; 283 | } 284 | 285 | // XML parsen 286 | libxml_use_internal_errors(true); 287 | $rss = @simplexml_load_string($xml); 288 | 289 | if (false === $rss) { 290 | $errors = libxml_get_errors(); 291 | $errorMsg = !empty($errors) ? $errors[0]->message : 'Invalid XML'; 292 | error_log('RSS Feed XML Error: ' . trim($errorMsg)); 293 | libxml_clear_errors(); 294 | return false; 295 | } 296 | 297 | $items = []; 298 | 299 | // RSS 2.0 Format 300 | if (isset($rss->channel->item)) { 301 | foreach ($rss->channel->item as $item) { 302 | $items[] = [ 303 | 'title' => (string) $item->title, 304 | 'link' => (string) $item->link, 305 | 'description' => (string) $item->description, 306 | 'pubDate' => (string) $item->pubDate, 307 | ]; 308 | } 309 | } 310 | // Atom Format 311 | elseif (isset($rss->entry)) { 312 | foreach ($rss->entry as $entry) { 313 | $link = (string) $entry->link['href'] ?: (string) $entry->link; 314 | $items[] = [ 315 | 'title' => (string) $entry->title, 316 | 'link' => $link, 317 | 'description' => (string) ($entry->summary ?: $entry->content), 318 | 'pubDate' => (string) ($entry->published ?: $entry->updated), 319 | ]; 320 | } 321 | } 322 | // Fallback: Prüfe andere Formate 323 | elseif (isset($rss->item)) { 324 | foreach ($rss->item as $item) { 325 | $items[] = [ 326 | 'title' => (string) $item->title, 327 | 'link' => (string) $item->link, 328 | 'description' => (string) $item->description, 329 | 'pubDate' => (string) $item->pubDate, 330 | ]; 331 | } 332 | } 333 | 334 | $feedTitle = (string) ($rss->channel->title ?? $rss->title ?? 'RSS-Feed'); 335 | 336 | return [ 337 | 'title' => $feedTitle, 338 | 'items' => $items, 339 | ]; 340 | } catch (Exception $e) { 341 | // Debug: Error-Log für Entwicklung 342 | error_log('RSS Feed Exception: ' . $e->getMessage()); 343 | return false; 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /lib/Items/Clock.php: -------------------------------------------------------------------------------- 1 | '; 26 | 27 | // Apple Watch Style Analog Clock Container 28 | $content .= '
'; 29 | 30 | // Digital Time Display (Apple Watch Style) 31 | $content .= '
'; 32 | $content .= '

'; 33 | $content .= '

'; 34 | $content .= 'Zeitzone: ' . $timezone . ''; 35 | $content .= '
'; 36 | 37 | $content .= ''; 38 | 39 | // CSS für die Apple Watch Style Uhr mit Dark Mode Support 40 | $content .= ''; 278 | 279 | // JavaScript für die Apple Watch Style Uhr 280 | $content .= ''; 401 | 402 | return $content; 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /assets/css/dashboard2-style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Dashboard - Dashboard 2.0 Design Integration 3 | * Modern Widget Dashboard Styles adapted from Dashboard2 4 | */ 5 | 6 | /* CSS Variables für Light/Dark Theme - Dashboard 2.0 Style */ 7 | :root { 8 | --dashboard-bg: #f8f9fa; 9 | --dashboard-widget-bg: #fff; 10 | --dashboard-widget-border: #ddd; 11 | --dashboard-text-color: #333; 12 | --dashboard-text-muted: #666; 13 | --dashboard-hover-bg: rgba(0,0,0,0.05); 14 | --dashboard-shadow: rgba(0,0,0,0.1); 15 | --dashboard-shadow-hover: rgba(0,0,0,0.15); 16 | --dashboard-primary: #337ab7; 17 | --dashboard-success: #5cb85c; 18 | --dashboard-warning: #f0ad4e; 19 | --dashboard-danger: #d9534f; 20 | } 21 | 22 | /* System Dark Mode */ 23 | @media (prefers-color-scheme: dark) { 24 | body:not(.rex-theme-light) { 25 | --dashboard-bg: #1a1a1a; 26 | --dashboard-widget-bg: rgba(50, 64, 80, 0.4); 27 | --dashboard-widget-border: rgba(255, 255, 255, 0.1); 28 | --dashboard-text-color: #e2e8f0; 29 | --dashboard-text-muted: #94a3b8; 30 | --dashboard-hover-bg: rgba(255,255,255,0.06); 31 | --dashboard-shadow: rgba(50, 64, 80, 0.3); 32 | --dashboard-shadow-hover: rgba(50, 64, 80, 0.4); 33 | } 34 | } 35 | 36 | /* REDAXO Light Theme */ 37 | body.rex-theme-light { 38 | --dashboard-bg: #f8f9fa !important; 39 | --dashboard-widget-bg: #fff !important; 40 | --dashboard-widget-border: #ddd !important; 41 | --dashboard-text-color: #333 !important; 42 | --dashboard-text-muted: #666 !important; 43 | --dashboard-hover-bg: rgba(0,0,0,0.05) !important; 44 | --dashboard-shadow: rgba(0,0,0,0.1) !important; 45 | --dashboard-shadow-hover: rgba(0,0,0,0.15) !important; 46 | } 47 | 48 | /* REDAXO Dark Theme */ 49 | body.rex-theme-dark { 50 | --dashboard-bg: #1a1a1a !important; 51 | --dashboard-widget-bg: rgba(50, 64, 80, 0.4) !important; 52 | --dashboard-widget-border: rgba(255, 255, 255, 0.1) !important; 53 | --dashboard-text-color: #e2e8f0 !important; 54 | --dashboard-text-muted: #94a3b8 !important; 55 | --dashboard-hover-bg: rgba(255,255,255,0.06) !important; 56 | --dashboard-shadow: rgba(50, 64, 80, 0.3) !important; 57 | --dashboard-shadow-hover: rgba(50, 64, 80, 0.4) !important; 58 | } 59 | 60 | /* Main Dashboard Override */ 61 | #rex-dashboard { 62 | background: transparent; 63 | margin: 0; 64 | } 65 | 66 | /* Grid-Stack Item Redesign - Dashboard 2.0 Style */ 67 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel { 68 | background: var(--dashboard-widget-bg); 69 | border: 1px solid var(--dashboard-widget-border); 70 | border-radius: 8px; 71 | box-shadow: 0 2px 8px var(--dashboard-shadow); 72 | transition: all 0.2s ease; 73 | overflow: hidden; 74 | } 75 | 76 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel:hover { 77 | box-shadow: 0 4px 16px var(--dashboard-shadow-hover); 78 | transform: translateY(-1px); 79 | } 80 | 81 | /* Panel Header Redesign */ 82 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-heading, 83 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel header.panel-heading { 84 | background: var(--dashboard-widget-bg); 85 | border-bottom: 1px solid var(--dashboard-widget-border); 86 | color: var(--dashboard-text-color); 87 | font-weight: 600; 88 | padding: 12px 16px; 89 | position: relative; 90 | border-top-left-radius: 8px; 91 | border-top-right-radius: 8px; 92 | } 93 | 94 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-title { 95 | color: var(--dashboard-text-color); 96 | font-size: 14px; 97 | font-weight: 600; 98 | margin: 0; 99 | padding-right: 60px; /* Platz für Actions schaffen */ 100 | } 101 | 102 | /* Light Theme - Blaue Header für alle panel-heading Elemente - MAXIMUM SPECIFICITY */ 103 | body.rex-theme-light #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-heading, 104 | body.rex-theme-light #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel header.panel-heading, 105 | body.rex-theme-light #rex-dashboard .panel .panel-heading, 106 | body.rex-theme-light #rex-dashboard .panel header.panel-heading, 107 | body.rex-theme-light #rex-dashboard header.panel-heading, 108 | body.rex-theme-light #rex-dashboard .panel-heading { 109 | background: #4b9ad9 !important; 110 | background-color: #4b9ad9 !important; 111 | color: white !important; 112 | border-bottom: 1px solid #3a8bc8 !important; 113 | border-bottom-color: #3a8bc8 !important; 114 | } 115 | 116 | body.rex-theme-light #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-title, 117 | body.rex-theme-light #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel header.panel-heading .panel-title, 118 | body.rex-theme-light #rex-dashboard .panel .panel-title, 119 | body.rex-theme-light #rex-dashboard .panel header.panel-heading .panel-title, 120 | body.rex-theme-light #rex-dashboard header.panel-heading .panel-title, 121 | body.rex-theme-light #rex-dashboard .panel-heading .panel-title { 122 | color: white !important; 123 | } 124 | 125 | /* Light Theme - Actions in blauem Header - MAXIMUM SPECIFICITY */ 126 | body.rex-theme-light #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-heading .actions > *, 127 | body.rex-theme-light #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel header.panel-heading .actions > *, 128 | body.rex-theme-light #rex-dashboard .panel .panel-heading .actions > *, 129 | body.rex-theme-light #rex-dashboard .panel header.panel-heading .actions > *, 130 | body.rex-theme-light #rex-dashboard header.panel-heading .actions > *, 131 | body.rex-theme-light #rex-dashboard .panel-heading .actions > * { 132 | background: rgba(255, 255, 255, 0.15) !important; 133 | background-color: rgba(255, 255, 255, 0.15) !important; 134 | color: rgba(255, 255, 255, 0.8) !important; 135 | } 136 | 137 | body.rex-theme-light #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-heading .actions > *:hover, 138 | body.rex-theme-light #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel header.panel-heading .actions > *:hover, 139 | body.rex-theme-light #rex-dashboard .panel .panel-heading .actions > *:hover, 140 | body.rex-theme-light #rex-dashboard .panel header.panel-heading .actions > *:hover, 141 | body.rex-theme-light #rex-dashboard header.panel-heading .actions > *:hover, 142 | body.rex-theme-light #rex-dashboard .panel-heading .actions > *:hover { 143 | background: rgba(255, 255, 255, 0.25) !important; 144 | background-color: rgba(255, 255, 255, 0.25) !important; 145 | color: white !important; 146 | } 147 | 148 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-title { 149 | color: var(--dashboard-text-color); 150 | font-size: 14px; 151 | font-weight: 600; 152 | margin: 0; 153 | padding-right: 60px; /* Platz für Actions schaffen */ 154 | } 155 | 156 | /* Panel Body Redesign */ 157 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-body { 158 | background: var(--dashboard-widget-bg); 159 | color: var(--dashboard-text-color); 160 | padding: 16px; 161 | overflow: auto; 162 | } 163 | 164 | /* Panel Footer */ 165 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-footer { 166 | background: var(--dashboard-widget-bg); 167 | border-top: 1px solid var(--dashboard-widget-border); 168 | color: var(--dashboard-text-muted); 169 | padding: 8px 16px; 170 | font-size: 12px; 171 | } 172 | 173 | /* Actions in Header */ 174 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-heading .actions { 175 | opacity: 0; 176 | position: absolute; 177 | top: 50%; 178 | right: 16px; 179 | transform: translateY(-50%); 180 | transition: opacity 0.2s ease; 181 | display: flex; 182 | align-items: center; 183 | pointer-events: none; 184 | gap: 4px; 185 | z-index: 5; 186 | } 187 | 188 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel:hover .panel-heading .actions { 189 | opacity: 1; 190 | pointer-events: auto; 191 | } 192 | 193 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-heading .actions > * { 194 | background: var(--dashboard-hover-bg); 195 | border-radius: 4px; 196 | color: var(--dashboard-text-muted); 197 | cursor: pointer; 198 | padding: 4px 6px; 199 | transition: all 0.2s ease; 200 | font-size: 12px; 201 | line-height: 1; 202 | min-width: auto; 203 | display: flex; 204 | align-items: center; 205 | justify-content: center; 206 | } 207 | 208 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-heading .actions > *:hover { 209 | background: var(--dashboard-text-muted); 210 | color: var(--dashboard-widget-bg); 211 | transform: scale(1.05); 212 | } 213 | 214 | /* Panel Title Padding für Actions */ 215 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-title { 216 | color: var(--dashboard-text-color); 217 | font-size: 14px; 218 | font-weight: 600; 219 | margin: 0; 220 | padding-right: 60px; /* Platz für Actions schaffen */ 221 | } 222 | 223 | /* Toolbar Redesign - Compact & Right Aligned mit Rahmen */ 224 | #rex-dashboard-settings { 225 | background: var(--dashboard-widget-bg) !important; 226 | border: 1px solid var(--dashboard-widget-border) !important; 227 | border-radius: 8px !important; 228 | box-shadow: 0 2px 8px var(--dashboard-shadow) !important; 229 | margin-bottom: 20px !important; 230 | padding: 12px 16px !important; 231 | display: flex !important; 232 | justify-content: flex-end !important; 233 | align-items: center !important; 234 | width: auto !important; 235 | max-width: 300px !important; 236 | margin-left: auto !important; 237 | margin-right: 0 !important; 238 | } 239 | 240 | #rex-dashboard-settings .actions { 241 | display: flex !important; 242 | align-items: center !important; 243 | gap: 4px !important; 244 | margin: 0 !important; 245 | } 246 | 247 | #rex-dashboard-settings ul.actions { 248 | display: flex !important; 249 | align-items: center !important; 250 | gap: 4px !important; 251 | margin: 0 !important; 252 | padding: 0 !important; 253 | list-style: none !important; 254 | } 255 | 256 | #rex-dashboard-settings ul.actions li { 257 | background: var(--dashboard-hover-bg) !important; 258 | border-radius: 6px !important; 259 | color: var(--dashboard-text-muted) !important; 260 | cursor: pointer !important; 261 | list-style: none !important; 262 | padding: 8px 10px !important; 263 | position: relative !important; 264 | transition: all 0.2s ease !important; 265 | display: inline-block !important; 266 | margin: 0 !important; 267 | } 268 | 269 | #rex-dashboard-settings ul.actions li:hover { 270 | background: var(--dashboard-text-muted) !important; 271 | color: var(--dashboard-widget-bg) !important; 272 | transform: translateY(-1px) !important; 273 | } 274 | 275 | /* Bootstrap Select auch im Rahmen */ 276 | #rex-dashboard-settings > .bootstrap-select.form-control:not([class*="col-"]) { 277 | width: auto !important; 278 | margin-right: 8px !important; 279 | } 280 | 281 | #rex-dashboard-settings > .bootstrap-select .btn { 282 | background: var(--dashboard-hover-bg) !important; 283 | border: none !important; 284 | border-radius: 6px !important; 285 | color: var(--dashboard-text-color) !important; 286 | font-size: 13px !important; 287 | padding: 8px 12px !important; 288 | transition: all 0.2s ease !important; 289 | } 290 | 291 | #rex-dashboard-settings > .bootstrap-select .btn:hover, 292 | #rex-dashboard-settings > .bootstrap-select.open .btn { 293 | background: var(--dashboard-text-muted) !important; 294 | color: var(--dashboard-widget-bg) !important; 295 | transform: translateY(-1px) !important; 296 | } 297 | 298 | /* Tooltips */ 299 | #rex-dashboard-settings ul.actions li span { 300 | background: var(--dashboard-text-color); 301 | border-radius: 4px; 302 | color: var(--dashboard-widget-bg); 303 | font-size: 12px; 304 | opacity: 0; 305 | padding: 4px 8px; 306 | pointer-events: none; 307 | position: absolute; 308 | right: 0; 309 | top: -8px; 310 | transform: translateY(-100%); 311 | transition: all 0.2s ease; 312 | white-space: nowrap; 313 | z-index: 1000; 314 | } 315 | 316 | #rex-dashboard-settings ul.actions li:hover span { 317 | opacity: 1; 318 | transform: translateY(-100%) translateY(-4px); 319 | } 320 | 321 | /* Bootstrap Select Widget Selector - auch rechts und kompakt */ 322 | #rex-dashboard-settings > .bootstrap-select.form-control:not([class*="col-"]) { 323 | width: auto; 324 | margin-right: 12px; 325 | } 326 | 327 | #rex-dashboard-settings > .bootstrap-select .btn { 328 | background: var(--dashboard-widget-bg); 329 | border: 1px solid var(--dashboard-widget-border); 330 | border-radius: 6px; 331 | box-shadow: 0 1px 3px var(--dashboard-shadow); 332 | color: var(--dashboard-text-color); 333 | font-size: 13px; 334 | padding: 8px 12px; 335 | transition: all 0.2s ease; 336 | } 337 | 338 | #rex-dashboard-settings > .bootstrap-select .btn:hover, 339 | #rex-dashboard-settings > .bootstrap-select.open .btn { 340 | background: var(--dashboard-hover-bg); 341 | border-color: var(--dashboard-text-muted); 342 | box-shadow: 0 2px 6px var(--dashboard-shadow-hover); 343 | transform: translateY(-1px); 344 | } 345 | 346 | /* Tooltips */ 347 | #rex-dashboard-settings ul.actions li span { 348 | background: var(--dashboard-text-color); 349 | border-radius: 4px; 350 | color: var(--dashboard-widget-bg); 351 | font-size: 12px; 352 | opacity: 0; 353 | padding: 4px 8px; 354 | pointer-events: none; 355 | position: absolute; 356 | right: 0; 357 | top: -8px; 358 | transform: translateY(-100%); 359 | transition: all 0.2s ease; 360 | white-space: nowrap; 361 | z-index: 1000; 362 | } 363 | 364 | #rex-dashboard-settings ul.actions li:hover span { 365 | opacity: 1; 366 | transform: translateY(-100%) translateY(-4px); 367 | } 368 | 369 | /* Table Styles */ 370 | #rex-dashboard .table { 371 | background: transparent; 372 | color: var(--dashboard-text-color); 373 | } 374 | 375 | #rex-dashboard .table th { 376 | background: transparent; 377 | border-bottom: 1px solid var(--dashboard-widget-border); 378 | color: var(--dashboard-text-color); 379 | font-weight: 600; 380 | } 381 | 382 | #rex-dashboard .table td { 383 | border-top: 1px solid var(--dashboard-widget-border); 384 | color: var(--dashboard-text-color); 385 | } 386 | 387 | #rex-dashboard .table-condensed td, 388 | #rex-dashboard .table-condensed th { 389 | padding: 6px; 390 | } 391 | 392 | /* Alert Styles */ 393 | #rex-dashboard .alert { 394 | border-radius: 6px; 395 | color: var(--dashboard-text-color); 396 | } 397 | 398 | #rex-dashboard .alert-info { 399 | background-color: rgba(91, 192, 222, 0.1); 400 | border-color: rgba(91, 192, 222, 0.3); 401 | } 402 | 403 | #rex-dashboard .alert-warning { 404 | background-color: rgba(240, 173, 78, 0.1); 405 | border-color: rgba(240, 173, 78, 0.3); 406 | } 407 | 408 | #rex-dashboard .alert-danger { 409 | background-color: rgba(217, 83, 79, 0.1); 410 | border-color: rgba(217, 83, 79, 0.3); 411 | } 412 | 413 | #rex-dashboard .alert-success { 414 | background-color: rgba(92, 184, 92, 0.1); 415 | border-color: rgba(92, 184, 92, 0.3); 416 | } 417 | 418 | /* Badge/Label Styles */ 419 | #rex-dashboard .label { 420 | border-radius: 4px; 421 | font-size: 11px; 422 | font-weight: 600; 423 | padding: 2px 6px; 424 | } 425 | 426 | #rex-dashboard .label-danger { 427 | background-color: var(--dashboard-danger); 428 | color: white; 429 | } 430 | 431 | #rex-dashboard .label-warning { 432 | background-color: var(--dashboard-warning); 433 | color: white; 434 | } 435 | 436 | #rex-dashboard .label-info { 437 | background-color: var(--dashboard-primary); 438 | color: white; 439 | } 440 | 441 | #rex-dashboard .label-success { 442 | background-color: var(--dashboard-success); 443 | color: white; 444 | } 445 | 446 | /* Text Colors */ 447 | #rex-dashboard .text-muted { 448 | color: var(--dashboard-text-muted) !important; 449 | } 450 | 451 | #rex-dashboard .text-primary { 452 | color: var(--dashboard-primary) !important; 453 | } 454 | 455 | /* Loading Animation */ 456 | #rex-dashboard .grid-stack > .grid-stack-item.loading .panel-body::after { 457 | background-color: rgba(var(--dashboard-widget-bg), 0.9); 458 | } 459 | 460 | #rex-dashboard .grid-stack > .grid-stack-item.loading .panel-body::before { 461 | color: var(--dashboard-text-muted); 462 | } 463 | 464 | /* Auto-refresh Animation */ 465 | #rex-dashboard-settings ul.actions li#rex-dashboard-auto-refresh.active i { 466 | animation: dashboard-pulse 2s infinite; 467 | color: var(--dashboard-success); 468 | } 469 | 470 | #rex-dashboard-settings ul.actions li#rex-dashboard-auto-refresh.paused i { 471 | color: var(--dashboard-warning); 472 | } 473 | 474 | @keyframes dashboard-pulse { 475 | 0% { opacity: 1; } 476 | 50% { opacity: 0.5; } 477 | 100% { opacity: 1; } 478 | } 479 | 480 | /* Clock Widget Specific */ 481 | .clock-face { 482 | background: var(--dashboard-widget-bg); 483 | border: 2px solid var(--dashboard-widget-border); 484 | box-shadow: 0 2px 8px var(--dashboard-shadow); 485 | } 486 | 487 | .clock-hand { 488 | background: var(--dashboard-text-color); 489 | } 490 | 491 | .second-hand { 492 | background: var(--dashboard-danger) !important; 493 | } 494 | 495 | .clock-number { 496 | color: var(--dashboard-text-color); 497 | } 498 | 499 | /* Responsive Improvements */ 500 | @media (max-width: 768px) { 501 | #rex-dashboard-settings { 502 | flex-direction: column; 503 | gap: 12px; 504 | } 505 | 506 | #rex-dashboard-settings ul.actions { 507 | justify-content: center; 508 | } 509 | 510 | #rex-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content .panel .panel-heading .actions { 511 | opacity: 1; 512 | pointer-events: auto; 513 | } 514 | } 515 | 516 | /* Smooth Transitions */ 517 | * { 518 | transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; 519 | } 520 | 521 | /* Widget Specific Enhancements */ 522 | #rex-dashboard .backup-status-widget .panel-success { 523 | border-color: rgba(92, 184, 92, 0.3); 524 | } 525 | 526 | #rex-dashboard .backup-status-widget .panel-warning { 527 | border-color: rgba(240, 173, 78, 0.3); 528 | } 529 | 530 | #rex-dashboard .backup-status-widget .panel-danger { 531 | border-color: rgba(217, 83, 79, 0.3); 532 | } 533 | 534 | /* Scrollbar Styling for Dark Theme */ 535 | #rex-dashboard .panel-body::-webkit-scrollbar { 536 | width: 6px; 537 | } 538 | 539 | #rex-dashboard .panel-body::-webkit-scrollbar-track { 540 | background: var(--dashboard-widget-border); 541 | border-radius: 3px; 542 | } 543 | 544 | #rex-dashboard .panel-body::-webkit-scrollbar-thumb { 545 | background: var(--dashboard-text-muted); 546 | border-radius: 3px; 547 | } 548 | 549 | #rex-dashboard .panel-body::-webkit-scrollbar-thumb:hover { 550 | background: var(--dashboard-text-color); 551 | } 552 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dashboard AddOn für REDAXO 5.x 2 | 3 | ## Version 2.0.0 - Modernisiertes Dashboard mit Standard-Widgets 4 | 5 | Das Dashboard AddOn ermöglicht es, wichtige Informationen aus REDAXO und anderen AddOns übersichtlich auf der Startseite des Backends anzuzeigen. Mit Version 2.0 wurde das Dashboard grundlegend modernisiert und um Standard-Widgets erweitert. 6 | 7 | ## ✨ Neue Features in Version 2.0 8 | 9 | ### 🎯 Standard-Widgets für REDAXO Core-Funktionen 10 | 11 | - **📝 Artikel-Widgets**: Zuletzt bearbeitete und neue Artikel mit Benutzerrechte-Integration 12 | - **📊 System-Status**: Speicherverbrauch, PHP-Version, Datenbankgröße (nur Admins) 13 | - **📁 Medien-Speicher**: Kategorisierte Übersicht des Medienpools nach Dateitypen 14 | - **🔧 AddOn-Verwaltung**: Verfügbare Updates und AddOn-Statistiken (nur Admins) 15 | - **📡 RSS-Feed**: Konfigurierbare RSS-Feeds mit Paginierung 16 | - **📈 Artikel-Status**: Übersicht über Online/Offline-Artikel 17 | 18 | ### 🛡️ Sicherheit und Berechtigungen 19 | 20 | - **Benutzerrechte**: Artikel-Widgets berücksichtigen REDAXO-Benutzerberechtigungen 21 | - **Admin-Widgets**: Sensitive Informationen nur für Administratoren sichtbar 22 | - **Structure-Permissions**: Integration mit `rex_complex_perm('structure')` 23 | 24 | ### 🎨 Verbessertes UI/UX 25 | 26 | - **Auto-Refresh**: Dashboard lädt automatisch aktuelle Daten beim Aufrufen 27 | - **Responsive Design**: Optimiert für verschiedene Bildschirmgrößen 28 | - **Drag & Drop**: Widgets frei positionierbar mit GridStack.js 29 | - **Konfigurationsseite**: Zentrale Verwaltung aller Widget-Einstellungen 30 | 31 | ## 🚀 Installation und Aktivierung 32 | 33 | 1. AddOn installieren und aktivieren 34 | 2. Als Administrator zu **Dashboard > Konfiguration** gehen 35 | 3. "Default Widgets aktivieren" ankreuzen 36 | 4. Gewünschte Widgets auswählen und Größen konfigurieren 37 | 5. Für RSS-Widget: Feed-URL und Namen eingeben 38 | 39 | ## 📋 Verfügbare Standard-Widgets 40 | 41 | | Widget | Beschreibung | Berechtigung | Größe | 42 | |--------|-------------|--------------|-------| 43 | | **Zuletzt aktualisierte Artikel** | Die 10 neuesten bearbeiteten Artikel | Structure-Rechte | 2 Spalten | 44 | | **Neue Artikel (30 Tage)** | Artikel der letzten 30 Tage | Structure-Rechte | 2 Spalten | 45 | | **Medien-Speicherverbrauch** | Dateityp-kategorisierte Übersicht | Alle User | 1 Spalte | 46 | | **Artikel-Status** | Online/Offline Artikel-Statistik | Alle User | 1 Spalte | 47 | | **System-Status** | PHP, MySQL, Speicher-Infos | Nur Admins | 2 Spalten | 48 | | **AddOn Updates** | Verfügbare Updates anzeigen | Nur Admins | 2 Spalten | 49 | | **AddOn Statistiken** | Installierte AddOns-Übersicht | Nur Admins | 1 Spalte | 50 | | **RSS Feed** | Konfigurierbare RSS-Feeds | Alle User | 1-2 Spalten | 51 | 52 | ## ⚙️ Konfiguration 53 | 54 | ### Dashboard-Einstellungen 55 | 56 | Über **Dashboard > Konfiguration** können Administratoren: 57 | 58 | - Default Widgets aktivieren/deaktivieren 59 | - Widget-Größen konfigurieren (1 oder 2 Spalten) 60 | - RSS-Feed URL und Namen festlegen 61 | - Demo-Widgets aktivieren (zu Testzwecken) 62 | 63 | ### RSS-Widget Konfiguration 64 | 65 | ``` 66 | RSS Feed URL: https://example.com/feed.xml 67 | RSS Feed Name: Mein RSS Feed 68 | Größe: 1 Spalte (klein) oder 2 Spalten (breit) 69 | ``` 70 | 71 | Das RSS-Widget zeigt 2 Items pro Seite mit Paginierung an. 72 | 73 | ## 🔧 Entwickler-API 74 | 75 | Für zukunftssichere Jedes Widget als eigene Datei im Lib-Verzeichnis eines Addons ablegen und dabei die NAmesapce-Empfehlungen einhalten 76 | 77 | ### Eigene Widgets erstellen 78 | 79 | Widgets basieren auf einem vorhandenen Widget oder auf einem der Basis-Elemente. 80 | 81 | ```php 82 | namespace MyWorkspace\MyAddon\Dashbaord; 83 | 84 | use FriendsOfRedaxo\Dashboard\Base\Item; 85 | 86 | class MeinCustomWidget extends Item 87 | { 88 | public function getTitle(): string 89 | { 90 | return 'Mein Custom Widget'; 91 | } 92 | 93 | public function getData() 94 | { 95 | return '

Hier steht der Inhalt

'; 96 | } 97 | } 98 | ``` 99 | 100 | ### Widget registrieren 101 | 102 | ```php 103 | use FriendsOfRedaxo\Dashboard\Dashboard; 104 | use MyWorkspace\MyAddon\Dashbaord\MeinCustomWidget; 105 | 106 | // In der boot.php des eigenen AddOns 107 | if (rex::isBackend() && rex_addon::exists('dashboard')) { 108 | Dashboard::addItem( 109 | MeinCustomWidget::factory('mein-widget-id', 'Mein Widget') 110 | ->setColumns(2) // 1 oder 2 Spalten 111 | ); 112 | } 113 | ``` 114 | 115 | ### Verfügbare Basis-Klassen 116 | 117 | Die Basisklassen sind im Namespace `FriendsOfRedaxo\Dashboard\Base`allokiert. 118 | 119 | - `Item` - Basis-Widget 120 | - `ChartBar` - Balkendiagramm 121 | - `ChartLine` - Liniendiagramm 122 | - `ChartPie` - Kreisdiagramm 123 | - `Table` - Tabellen-Widget 124 | 125 | ## 📊 Widget-Typen im Detail 126 | 127 | ### Chart-Widgets 128 | 129 | ```php 130 | namespace MyWorkspace\MyAddon\Dashbaord; 131 | 132 | use FriendsOfRedaxo\Dashboard\Base\ChartBar; 133 | 134 | class MeinChartWidget extends ChartBar 135 | { 136 | public function getChartData() 137 | { 138 | return [ 139 | 'Label 1' => 42, 140 | 'Label 2' => 37, 141 | 'Label 3' => 28 142 | ]; 143 | } 144 | } 145 | ``` 146 | 147 | ### Tabellen-Widgets 148 | 149 | ```php 150 | namespace MyWorkspace\MyAddon\Dashbaord; 151 | 152 | use FriendsOfRedaxo\Dashboard\Base\Table; 153 | 154 | class MeinTabellenWidget extends Table 155 | { 156 | public function getTableData() 157 | { 158 | return [ 159 | 'headers' => ['Name', 'Wert', 'Status'], 160 | 'rows' => [ 161 | ['Eintrag 1', '100', 'Aktiv'], 162 | ['Eintrag 2', '200', 'Inaktiv'] 163 | ] 164 | ]; 165 | } 166 | } 167 | ``` 168 | 169 | ## 🔐 Berechtigungen und Sicherheit 170 | 171 | ### Structure-Berechtigungen 172 | 173 | Artikel-Widgets prüfen automatisch: 174 | 175 | ```php 176 | $user = rex::requireUser(); 177 | $structurePerm = $user->getComplexPerm('structure'); 178 | if ($structurePerm->hasCategoryPerm($categoryId)) { 179 | // User hat Zugriff auf diese Kategorie 180 | } 181 | ``` 182 | 183 | ### Admin-Only Widgets 184 | 185 | ```php 186 | use FriendsOfRedaxo\Dashboard\Dashboard; 187 | 188 | if (rex::getUser() && rex::getUser()->isAdmin()) { 189 | // Widget nur für Admins registrieren 190 | Dashboard::addItem(AdminWidget::factory('admin-widget', 'Admin Widget')); 191 | } 192 | ``` 193 | 194 | ## 🎛️ Erweiterte Features 195 | 196 | ### Auto-Refresh 197 | 198 | - Dashboard refresht automatisch beim Laden (500ms Verzögerung) 199 | - Automatisches Refresh alle 5 Minuten 200 | - Manueller Refresh über Refresh-Button 201 | 202 | ### GridStack Integration 203 | 204 | - Drag & Drop Positionierung 205 | - Automatische Größenanpassung 206 | - Benutzer-spezifische Layouts (je User individuell gespeichert) 207 | - Responsive Breakpoints 208 | 209 | ### Multi-Language Support 210 | 211 | - Widgets passen sich automatisch an verfügbare Sprachen an 212 | - Sprachenspalten werden nur angezeigt wenn > 1 Sprache vorhanden 213 | 214 | ## 📱 Responsive Design 215 | 216 | Das Dashboard passt sich automatisch an verschiedene Bildschirmgrößen an: 217 | 218 | - **Desktop**: Vollständiges Grid mit Drag & Drop 219 | - **Tablet**: Optimierte Spaltenbreiten 220 | - **Mobile**: Single-Column Layout mit vertikalem Scrolling 221 | 222 | ## 🔄 Migration von Version 1.x 223 | 224 | ### Automatische Migration 225 | 226 | - Demo-Plugin wird automatisch aufgelöst 227 | - Bestehende Widget-Positionen bleiben erhalten 228 | - Neue Standard-Widgets werden deaktiviert hinzugefügt 229 | 230 | ### Breaking Changes 231 | 232 | - Demo-Plugin entfernt (Funktionalität ins Core integriert) 233 | - Neue Konfigurationsstruktur 234 | - Widget-IDs geändert (`dashboard-default-*` Präfix) 235 | 236 | ## 🐛 Debugging 237 | 238 | ### Debug-Modus 239 | 240 | Im REDAXO Debug-Modus werden zusätzliche Informationen angezeigt: 241 | 242 | - Widget-Ladezeiten 243 | - Berechtigungsprüfungen 244 | - Cache-Status 245 | - JavaScript-Fehler 246 | 247 | ### Log-Dateien 248 | 249 | Fehler werden in REDAXO's System-Log geschrieben: 250 | ``` 251 | redaxo/data/log/system.log 252 | ``` 253 | 254 | ## 🤝 Kompatibilität 255 | 256 | - **REDAXO**: >= 5.11.0 257 | - **PHP**: >= 7.4 258 | - **Browser**: Moderne Browser mit ES6-Support 259 | - **Mobile**: iOS Safari, Chrome Mobile, Firefox Mobile 260 | 261 | ## 📄 Lizenz 262 | 263 | MIT License - siehe LICENSE-Datei 264 | 265 | ## 👥 Credits 266 | 267 | - **Entwicklung**: Friends of REDAXO 268 | - **GridStack**: https://gridstackjs.com/ 269 | - **Chart.js**: https://www.chartjs.org/ 270 | - **Bootstrap**: https://getbootstrap.com/ 271 | 272 | ## 📞 Support 273 | 274 | - **GitHub**: https://github.com/FriendsOfRedaxo/dashboard 275 | - **REDAXO Slack**: #addons Channel 276 | - **Forum**: https://redaxo.org/forum/ 277 | 278 | --- 279 | 280 | **Dashboard AddOn 2.0** - Modernes Dashboard für REDAXO 5.x mit Standard-Widgets, Sicherheitsfeatures und verbessertem UX. 281 | 282 | ### Ein einfaches Info-Item (Text anzeigen) 283 | 284 | siehe DemoItems\Info 285 | 286 | ```php 287 | namespace FriendsOfRedaxo\Dashboard\DemoItems; 288 | 289 | use FriendsOfRedaxo\Dashboard\Base\Item; 290 | 291 | class Info extends Item 292 | { 293 | public function getData() 294 | { 295 | return 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.'; 296 | } 297 | } 298 | 299 | ``` 300 | 301 | ### Ein Dashbord-Element: Balkendiagramm 302 | 303 | ```php 304 | namespace MyWorkspace\MyAddon\Dashbaord; 305 | 306 | use FriendsOfRedaxo\Dashboard\Base\ChartBar; 307 | 308 | class MeinBalkenDiagramm extends ChartBar 309 | { 310 | protected function __construct($id, $name) 311 | { 312 | parent::__construct($id, $name); 313 | $this->setHorizontal(); // optional, sonst vertikal 314 | } 315 | 316 | public function getChartData() 317 | { 318 | return [ 319 | 'Rot' => rand(1,122), 320 | 'Blau' => rand(1,122), 321 | 'Gelb' => rand(1,122), 322 | 'Grün' => rand(1,122), 323 | 'Lila' => rand(1,122), 324 | 'Orange' => rand(1,122), 325 | ]; 326 | } 327 | } 328 | 329 | ``` 330 | 331 | ### Ein Dashbord-Element: Liniendiagramm 332 | 333 | ```php 334 | namespace MyWorkspace\MyAddon\Dashbaord; 335 | 336 | use FriendsOfRedaxo\Dashboard\Base\ChartLine; 337 | 338 | class MeinLinienDiagramm extends ChartLine 339 | { 340 | public function getChartData() 341 | { 342 | return [ 343 | 'Linie 1' => [ 344 | 'Rot' => 12, 345 | 'Blau' => 19, 346 | 'Gelb' => 3, 347 | 'Grün' => 5, 348 | 'Lila' => 2, 349 | 'Orange' => 3, 350 | ], 351 | 'Linie 2' => [ 352 | 'Rot' => 3, 353 | 'Blau' => 5, 354 | 'Gelb' => 8, 355 | 'Grün' => 10, 356 | 'Lila' => 11, 357 | 'Orange' => 11.5, 358 | ], 359 | 'Linie 3' => [ 360 | 'Rot' => 5, 361 | 'Blau' => 13, 362 | 'Gelb' => 16, 363 | 'Grün' => 12, 364 | 'Lila' => 7, 365 | 'Orange' => 2, 366 | ] 367 | ]; 368 | } 369 | } 370 | 371 | ``` 372 | 373 | ### Ein Dashboard-Element: Tortendiagramm 374 | 375 | ```php 376 | namespace MyWorkspace\MyAddon\Dashbaord; 377 | 378 | use FriendsOfRedaxo\Dashboard\Base\ChartPie; 379 | 380 | class MeinLinienDiagramm extends ChartPie 381 | 382 | class MeinTortenDiagramm extends ChartPie 383 | { 384 | public function getChartData() 385 | { 386 | return [ 387 | 'Rot' => 12, 388 | 'Blau' => 19, 389 | 'Gelb' => 3, 390 | 'Grün' => 5, 391 | 'Lila' => 2, 392 | 'Orange' => 3, 393 | ]; 394 | } 395 | } 396 | 397 | ``` 398 | ### Ein Dashboard-Element: Datentabelle 399 | 400 | ```php 401 | namespace MyWorkspace\MyAddon\Dashbaord; 402 | 403 | use FriendsOfRedaxo\Dashboard\Base\Table; 404 | 405 | class MeineDatenTabelle extends Table 406 | { 407 | protected $header = []; 408 | protected $data = []; 409 | 410 | protected function getTableData() 411 | { 412 | $tableData = rex_sql::factory()->setQuery(' 413 | SELECT id ID 414 | , label Label 415 | , dbtype `DB-Type` 416 | FROM rex_metainfo_type 417 | ORDER BY id ASC 418 | ')->getArray(); 419 | 420 | if (!empty($tableData)) { 421 | $this->data = $tableData; 422 | $this->header = array_keys($tableData[0]); 423 | } 424 | 425 | return [ 426 | 'data' => $this->data, 427 | 'header' => $this->header, 428 | ]; 429 | } 430 | } 431 | 432 | ``` 433 | 434 | ## Anmeldung der eigenen Widgets 435 | 436 | In der boot.php des eigenen Project-AddOns oder in der jeweiligen boot.php des entsprechenden AddOns (Für AddOn-Entwickler) müssen die entsprechenden Widgets angemeldet werden. 437 | 438 | Hier ein Beispiel für die Anmeldung der Widgets aus dem DemoPlugin, siehe oben: 439 | 440 | ```php 441 | use FriendsOfRedaxo\Dashboard\Dashboard; 442 | use use FriendsOfRedaxo\Dashboard\DemoItems\Info; 443 | use use FriendsOfRedaxo\Dashboard\DemoItems\ChartBarHorizontal; 444 | use use FriendsOfRedaxo\Dashboard\DemoItems\ChartBarVertical; 445 | use use FriendsOfRedaxo\Dashboard\DemoItems\ChartPie; 446 | use use FriendsOfRedaxo\Dashboard\DemoItems\ChartLine; 447 | use use FriendsOfRedaxo\Dashboard\DemoItems\BigNumber; 448 | 449 | if (rex::isBackend() && rex_addon::exists('dashboard')) { 450 | 451 | Dashboard::addItem( 452 | Info::factory('dashboard-demo-1', 'Demo 1'), 453 | ); 454 | 455 | Dashboard::addItem( 456 | Info::factory('dashboard-demo-2', 'Demo 2') 457 | ->setColumns(2), 458 | ); 459 | 460 | Dashboard::addItem( 461 | Info::factory('dashboard-demo-3', 'Demo 3') 462 | ->setColumns(3), 463 | ); 464 | 465 | Dashboard::addItem( 466 | ChartBarHorizontal::factory('dashboard-demo-chart-bar-horizontal', 'Chartdemo Balken horizontal'), 467 | ); 468 | 469 | Dashboard::addItem( 470 | ChartBarVertical::factory('dashboard-demo-chart-bar-vertical', 'Chartdemo Balken vertikal'), 471 | ); 472 | 473 | Dashboard::addItem( 474 | ChartPie::factory('dashboard-demo-chart-pie', 'Chartdemo Kreisdiagramm'), 475 | ); 476 | 477 | Dashboard::addItem( 478 | ChartPie::factory('dashboard-demo-chart-donut', 'Chartdemo Donutdiagramm') 479 | ->setDonut(), 480 | ); 481 | 482 | Dashboard::addItem( 483 | Table::factory('dashboard-demo-table-sql', 'Tabelle (SQL)') 484 | ->setTableAttribute('data-locale', 'de-DE'), 485 | ); 486 | 487 | Dashboard::addItem( 488 | ChartLine::factory('dashboard-demo-chart-line', 'Liniendiagramm'), 489 | ); 490 | 491 | Dashboard::addItem( 492 | BigNumber::factory('dashboard-demo-big-number', 'Big Number Demo') 493 | ->setColumns(1), // Als kleines Widget 494 | ); 495 | 496 | ); 497 | 498 | ``` 499 | 500 | ## Auswählen des Widgets im Dashboard 501 | 502 | Sobald die Widgets angemeldet sind, können sie im Dashboard ausgewählt und angeordnet werden. Dazu klickt man im Widget auf `Widget auswählen`. 503 | 504 | 505 | ## Umstellung auf Namespace 506 | 507 | Bis Version 2.1.3 waren wenige Klassen bereits in einen Namespace (`FriendsOfREDAXO/Dashboard`) angeordnet. Mit der Folgeversion 2.2 508 | sind alle Klassen einheitlich im Namespace `FriendsOfRedaxo/Dashboard` (andere Schreibweise) bzw. in Sub-Namespaces dazu eingeordnet. 509 | I.d.R. wurden die Klassen dabei auch umbenannt, denn Teile des Namens sind nun Teil des Namespace. 510 | 511 | Die bisherigen Klassen (z.B. `rex_dashboard`) existieren für eine Übergangszeit bis zur Version 3.0 weiter. Das Update auf Version 2.4 sollte daher keine Funktionseinschränkungen bezüglich eigener Elemente mit sich bringen. 512 | 513 | **Die Umstellung im eigenen Code auf die neuen Klassennamen und Namespaces sollte dennoch zeitnah erfolgen!** 514 | 515 | Hinweise zur allgemeinen Vorgehensweise sind z.B. auf [Github (FriendsOfRedaxo/Discussions)](https://github.com/orgs/FriendsOfREDAXO/discussions/40) zu finden. 516 | 517 | Die Tabelle stellt alte und neue Klassen gegenüber, jeweils mit Namespace: 518 | 519 | | Alt | Neu | 520 | |-----|-----| 521 | | rex_dashboard | FriendsOfRedaxo\Dashboard\Dashbord | 522 | | rex_dashboard_item | FriendsOfRedaxo\Dashboard\Base\Item | 523 | | rex_dashboard_item_chart | FriendsOfRedaxo\Dashboard\Base\Chart | 524 | | rex_dashboard_item_chart_bar | FriendsOfRedaxo\Dashboard\Base\ChartBar | 525 | | rex_dashboard_item_chart_line | FriendsOfRedaxo\Dashboard\Base\ChartLine | 526 | | rex_dashboard_item_chart_pie | FriendsOfRedaxo\Dashboard\Base\ChartPie | 527 | | rex_dashboard_item_table | FriendsOfRedaxo\Dashboard\Base\Table | 528 | | rex_dashboard_chart_colors | FriendsOfRedaxo\Dashboard\Traits\ChartColors | 529 | | FriendsOfREDAXO\Dashboard\DashboardDemo | FriendsOfRedaxo\Dashboard\DemoItems\DashboardDemo 530 | | FriendsOfREDAXO\Dashboard\rex_dashboard_item | FriendsOfRedaxo\Dashboard\DemoItems\Info | 531 | | FriendsOfREDAXO\Dashboard\DashboardItemDemoBigNumber | FriendsOfRedaxo\Dashboard\DemoItems\BigNumber | 532 | | FriendsOfREDAXO\Dashboard\DashboardItemDemoChartBarHorizontal | FriendsOfRedaxo\Dashboard\DemoItems\ChartBarHorizontal | 533 | | FriendsOfREDAXO\Dashboard\DashboardItemDemoChartBarVertical | FriendsOfRedaxo\Dashboard\DemoItems\ChartBarVertical | 534 | | FriendsOfREDAXO\Dashboard\DashboardItemDemoChartLine | FriendsOfRedaxo\Dashboard\DemoItems\ChartLine | 535 | | FriendsOfREDAXO\Dashboard\DashboardItemDemoChartPie | FriendsOfRedaxo\Dashboard\DemoItems\ChartPie | 536 | | FriendsOfREDAXO\Dashboard\DashboardItemDemoTable | FriendsOfRedaxo\Dashboard\DemoItems\Table | 537 | | FriendsOfREDAXO\Dashboard\DashboardDefault | FriendsOfRedaxo\Dashboard\DashboardDefault | 538 | | FriendsOfREDAXO\Dashboard\DashboardItemAddonStatistics | FriendsOfRedaxo\Dashboard\Items\AddonStatistics | 539 | | FriendsOfREDAXO\Dashboard\DashboardItemAddonUpdates | FriendsOfRedaxo\Dashboard\Items\AddonUpdates | 540 | | FriendsOfREDAXO\Dashboard\DashboardItemArticleStatus | FriendsOfRedaxo\Dashboard\Items\ArticleStatus | 541 | | FriendsOfREDAXO\Dashboard\DashboardItemBackupStatus | FriendsOfRedaxo\Dashboard\Items\BackupStatus | 542 | | FriendsOfREDAXO\Dashboard\DashboardItemBigNumberDemo | FriendsOfRedaxo\Dashboard\Items\BigNumberDemo | 543 | | FriendsOfREDAXO\Dashboard\DashboardItemClock | FriendsOfRedaxo\Dashboard\Items\Clock | 544 | | FriendsOfREDAXO\Dashboard\DashboardItemCountdownDemo | FriendsOfRedaxo\Dashboard\Items\CountdownDemo | 545 | | FriendsOfREDAXO\Dashboard\DashboardItemMediaStorage | FriendsOfRedaxo\Dashboard\Items\MediaStorage | 546 | | FriendsOfREDAXO\Dashboard\DashboardItemNewArticles | FriendsOfRedaxo\Dashboard\Items\NewArticles | 547 | | FriendsOfREDAXO\Dashboard\DashboardItemRecentArticles | FriendsOfRedaxo\Dashboard\Items\RecentArticles | 548 | | FriendsOfREDAXO\Dashboard\DashboardItemRssClean | FriendsOfRedaxo\Dashboard\Items\RssClean | 549 | | FriendsOfREDAXO\Dashboard\DashboardItemSystemStatus | FriendsOfRedaxo\Dashboard\Items\SystemStatus | 550 | | FriendsOfREDAXO\Dashboard\DashboardItemUserActivity | FriendsOfRedaxo\Dashboard\Items\UserActivity | 551 | | rex_api_dashboard_get | FriendsOfRedaxo\Dashboard\Api\Get | 552 | | rex_api_dashboard_store | FriendsOfRedaxo\Dashboard\Api\Store | 553 | --------------------------------------------------------------------------------