├── 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 | = $this->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 |
12 | -
13 |
14 | = rex_i18n::msg('dashboard_action_refresh') ?>
15 |
16 | -
20 |
21 | = rex_i18n::msg('dashboard_action_auto_refresh') ?>
22 |
23 | ">
24 |
25 | =rex_i18n::msg('dashboard_action_compact') ?>
26 |
27 | -
28 |
29 | =rex_i18n::msg('dashboard_action_autosize') ?>
30 |
*/ ?>
31 |
32 | = $this->getVar('configButton', '') ?>= $this->getVar('widgetSelect') ?>
33 |
34 | = $this->getVar('outputActive') ?>
35 | = $this->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 |
32 |
33 |
= $content ?>
34 |
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 .= '| ' . rex_i18n::msg('dashboard_article', 'Artikel') . ' | ';
74 | $content .= '' . rex_i18n::msg('dashboard_category', 'Kategorie') . ' | ';
75 |
76 | // Sprach-Spalte nur anzeigen wenn mehrere Sprachen existieren
77 | if (count(rex_clang::getAll()) > 1) {
78 | $content .= '' . rex_i18n::msg('dashboard_language', 'Sprache') . ' | ';
79 | }
80 |
81 | $content .= '' . rex_i18n::msg('dashboard_updated', 'Aktualisiert') . ' | ';
82 | $content .= '' . rex_i18n::msg('dashboard_by', 'Von') . ' | ';
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 .= '| ' . rex_escape($article['name']) . ' | ';
100 | $content .= '' . rex_escape($article['category_name'] ?: '-') . ' | ';
101 |
102 | // Sprach-Spalte nur anzeigen wenn mehrere Sprachen existieren
103 | if (count(rex_clang::getAll()) > 1) {
104 | $content .= '' . rex_escape($article['lang_name'] ?: '-') . ' | ';
105 | }
106 |
107 | $content .= '' . rex_formatter::strftime($article['updatedate'], 'datetime') . ' | ';
108 | $content .= '' . rex_escape($article['updateuser'] ?: '-') . ' | ';
109 | $content .= '
';
110 | }
111 |
112 | $content .= '';
113 | $content .= '
';
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 .= '| ' . rex_i18n::msg('dashboard_article', 'Artikel') . ' | ';
74 | $content .= '' . rex_i18n::msg('dashboard_category', 'Kategorie') . ' | ';
75 |
76 | // Sprach-Spalte nur anzeigen wenn mehrere Sprachen existieren
77 | if (count(rex_clang::getAll()) > 1) {
78 | $content .= '' . rex_i18n::msg('dashboard_language', 'Sprache') . ' | ';
79 | }
80 |
81 | $content .= '' . rex_i18n::msg('dashboard_created', 'Erstellt') . ' | ';
82 | $content .= '' . rex_i18n::msg('dashboard_by', 'Von') . ' | ';
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 .= '| ' . rex_escape($article['name']) . ' | ';
100 | $content .= '' . rex_escape($article['category_name'] ?: '-') . ' | ';
101 |
102 | // Sprach-Spalte nur anzeigen wenn mehrere Sprachen existieren
103 | if (count(rex_clang::getAll()) > 1) {
104 | $content .= '' . rex_escape($article['lang_name'] ?: '-') . ' | ';
105 | }
106 |
107 | $content .= '' . rex_formatter::strftime($article['createdate'], 'datetime') . ' | ';
108 | $content .= '' . rex_escape($article['createuser'] ?: '-') . ' | ';
109 | $content .= '
';
110 | }
111 |
112 | $content .= '';
113 | $content .= '
';
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 .= '| Letztes Backup: | ' . date('d.m.Y H:i', $lastSqlBackup['created']) . ' |
';
58 | $content .= '| Größe: | ' . $this->formatFileSize($lastSqlBackup['size']) . ' |
';
59 | $content .= '| Anzahl Backups: | ' . count($sqlBackups) . ' |
';
60 | } else {
61 | $content .= '| Keine SQL-Backups vorhanden |
';
62 | }
63 |
64 | $content .= '
';
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 .= '| Letztes Backup: | ' . date('d.m.Y H:i', $lastFileBackup['created']) . ' |
';
75 | $content .= '| Größe: | ' . $this->formatFileSize($lastFileBackup['size']) . ' |
';
76 | $content .= '| Anzahl Backups: | ' . count($fileBackups) . ' |
';
77 | } else {
78 | $content .= '| Keine Datei-Backups vorhanden |
';
79 | }
80 |
81 | $content .= '
';
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 .= '| REDAXO Version: | ' . $redaxoVersion . ' |
';
62 | $content .= '| PHP Version: | ' . $phpVersion . ' |
';
63 | $content .= '| Memory Limit: | ' . $memoryLimit . ' |
';
64 | $content .= '| Max Execution Time: | ' . $maxExecutionTime . 's |
';
65 | $content .= '
';
66 | $content .= '
';
67 |
68 | // Speicher Info
69 | $content .= '
';
70 | $content .= '
Speicher
';
71 | $content .= '
';
72 | $content .= '| Freier Speicher: | ' . $diskFreeSpace . ' |
';
73 | $content .= '| Gesamtspeicher: | ' . $diskTotalSpace . ' |
';
74 | $content .= '| Cache-Größe: | ' . $this->formatBytes($cacheSize) . ' |
';
75 | $content .= '
';
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 '';
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 '';
48 |
49 | // Einzelne Default Widgets mit Größen-Optionen
50 | $defaultWidgets = [
51 | 'default_recent_articles' => 'Zuletzt aktualisierte Artikel',
52 | 'default_new_articles' => 'Neue Artikel (30 Tage)',
53 | 'default_media_storage' => 'Medien-Speicherverbrauch (Chart)',
54 | 'default_article_status' => 'Artikel-Status (Chart)',
55 | 'default_system_status' => 'System-Status',
56 | 'default_user_activity' => 'Benutzer-Aktivität (Chart)',
57 | 'default_addon_updates' => 'AddOn Verwaltung (nur Admins)',
58 | ];
59 |
60 | foreach ($defaultWidgets as $configKey => $label) {
61 | // Widget aktivieren/deaktivieren
62 | $field = $form->addCheckboxField($configKey);
63 | $field->addOption($label, '1');
64 |
65 | // Größe (klein/breit) auswählen
66 | $sizeField = $form->addSelectField($configKey . '_columns', null, ['class' => 'form-control widget-size-select', 'data-widget' => $configKey]);
67 | $sizeField->setLabel('Größe (' . $label . ')');
68 | $select = $sizeField->getSelect();
69 | $select->addOption('Klein (1 Spalte)', '1');
70 | $select->addOption('Breit (2 Spalten)', '2');
71 | }
72 |
73 | // RSS Feed Widget
74 | $field = $form->addCheckboxField('default_rss_feed');
75 | $field->addOption('RSS Feed Widget', '1');
76 |
77 | // RSS Feed URL
78 | $field = $form->addTextField('rss_feed_url');
79 | $field->setLabel('RSS Feed URL');
80 | $field->setNotice('URL des RSS/Atom Feeds (z.B. https://example.com/feed.xml)');
81 |
82 | // RSS Feed Name
83 | $field = $form->addTextField('rss_feed_name');
84 | $field->setLabel('RSS Feed Name');
85 | $field->setNotice('Name des Feeds für die Anzeige im Dashboard');
86 |
87 | // RSS Feed Größe
88 | $sizeField = $form->addSelectField('default_rss_feed_columns', null, ['class' => 'form-control widget-size-select', 'data-widget' => 'default_rss_feed']);
89 | $sizeField->setLabel('Größe (RSS Feed Widget)');
90 | $select = $sizeField->getSelect();
91 | $select->addOption('Klein (1 Spalte)', '1');
92 | $select->addOption('Breit (2 Spalten)', '2');
93 |
94 | 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 = '';
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 = '';
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 |
--------------------------------------------------------------------------------