xref: /plugin/calendar/syntax.php (revision 231d0edb9148222207862b7b998861794a543cdd)
119378907SAtari911<?php
219378907SAtari911/**
319378907SAtari911 * DokuWiki Plugin calendar (Syntax Component)
419378907SAtari911 * Compact design with integrated event list
519378907SAtari911 *
619378907SAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
719378907SAtari911 * @author  DokuWiki Community
819378907SAtari911 */
919378907SAtari911
1019378907SAtari911if (!defined('DOKU_INC')) die();
1119378907SAtari911
1219378907SAtari911class syntax_plugin_calendar extends DokuWiki_Syntax_Plugin {
1319378907SAtari911
1419378907SAtari911    public function getType() {
1519378907SAtari911        return 'substition';
1619378907SAtari911    }
1719378907SAtari911
1819378907SAtari911    public function getPType() {
1919378907SAtari911        return 'block';
2019378907SAtari911    }
2119378907SAtari911
2219378907SAtari911    public function getSort() {
2319378907SAtari911        return 155;
2419378907SAtari911    }
2519378907SAtari911
2619378907SAtari911    public function connectTo($mode) {
2719378907SAtari911        $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
2819378907SAtari911        $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
2919378907SAtari911        $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
3019378907SAtari911    }
3119378907SAtari911
3219378907SAtari911    public function handle($match, $state, $pos, Doku_Handler $handler) {
3319378907SAtari911        $isEventList = (strpos($match, '{{eventlist') === 0);
3419378907SAtari911        $isEventPanel = (strpos($match, '{{eventpanel') === 0);
3519378907SAtari911
3619378907SAtari911        if ($isEventList) {
3719378907SAtari911            $match = substr($match, 12, -2);
3819378907SAtari911        } elseif ($isEventPanel) {
3919378907SAtari911            $match = substr($match, 13, -2);
4019378907SAtari911        } else {
4119378907SAtari911            $match = substr($match, 10, -2);
4219378907SAtari911        }
4319378907SAtari911
4419378907SAtari911        $params = array(
4519378907SAtari911            'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'),
4619378907SAtari911            'year' => date('Y'),
4719378907SAtari911            'month' => date('n'),
4819378907SAtari911            'namespace' => '',
4919378907SAtari911            'daterange' => '',
50e3a9f44cSAtari911            'date' => '',
51e3a9f44cSAtari911            'range' => ''
5219378907SAtari911        );
5319378907SAtari911
5419378907SAtari911        if (trim($match)) {
5519378907SAtari911            $pairs = preg_split('/\s+/', trim($match));
5619378907SAtari911            foreach ($pairs as $pair) {
5719378907SAtari911                if (strpos($pair, '=') !== false) {
5819378907SAtari911                    list($key, $value) = explode('=', $pair, 2);
5919378907SAtari911                    $params[trim($key)] = trim($value);
6087ac9bf3SAtari911                } else {
6187ac9bf3SAtari911                    // Handle standalone flags like "today"
6287ac9bf3SAtari911                    $params[trim($pair)] = true;
6319378907SAtari911                }
6419378907SAtari911            }
6519378907SAtari911        }
6619378907SAtari911
6719378907SAtari911        return $params;
6819378907SAtari911    }
6919378907SAtari911
7019378907SAtari911    public function render($mode, Doku_Renderer $renderer, $data) {
7119378907SAtari911        if ($mode !== 'xhtml') return false;
7219378907SAtari911
7319378907SAtari911        if ($data['type'] === 'eventlist') {
7419378907SAtari911            $html = $this->renderStandaloneEventList($data);
7519378907SAtari911        } elseif ($data['type'] === 'eventpanel') {
7619378907SAtari911            $html = $this->renderEventPanelOnly($data);
7719378907SAtari911        } else {
7819378907SAtari911            $html = $this->renderCompactCalendar($data);
7919378907SAtari911        }
8019378907SAtari911
8119378907SAtari911        $renderer->doc .= $html;
8219378907SAtari911        return true;
8319378907SAtari911    }
8419378907SAtari911
8519378907SAtari911    private function renderCompactCalendar($data) {
8619378907SAtari911        $year = (int)$data['year'];
8719378907SAtari911        $month = (int)$data['month'];
8819378907SAtari911        $namespace = $data['namespace'];
8919378907SAtari911
90e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
91e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
92e3a9f44cSAtari911
93e3a9f44cSAtari911        if ($isMultiNamespace) {
94e3a9f44cSAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
95e3a9f44cSAtari911        } else {
9619378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
97e3a9f44cSAtari911        }
9819378907SAtari911        $calId = 'cal_' . md5(serialize($data) . microtime());
9919378907SAtari911
10019378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
10119378907SAtari911
10219378907SAtari911        $prevMonth = $month - 1;
10319378907SAtari911        $prevYear = $year;
10419378907SAtari911        if ($prevMonth < 1) {
10519378907SAtari911            $prevMonth = 12;
10619378907SAtari911            $prevYear--;
10719378907SAtari911        }
10819378907SAtari911
10919378907SAtari911        $nextMonth = $month + 1;
11019378907SAtari911        $nextYear = $year;
11119378907SAtari911        if ($nextMonth > 12) {
11219378907SAtari911            $nextMonth = 1;
11319378907SAtari911            $nextYear++;
11419378907SAtari911        }
11519378907SAtari911
1161d05cddcSAtari911        $html = '<div class="calendar-compact-container" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '" data-year="' . $year . '" data-month="' . $month . '">';
1171d05cddcSAtari911
1181d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
1191d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
1201d05cddcSAtari911
1211d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
1221d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
12319378907SAtari911
12419378907SAtari911        // Embed events data as JSON for JavaScript access
12519378907SAtari911        $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>';
12619378907SAtari911
12719378907SAtari911        // Left side: Calendar
12819378907SAtari911        $html .= '<div class="calendar-compact-left">';
12919378907SAtari911
13019378907SAtari911        // Header with navigation
13119378907SAtari911        $html .= '<div class="calendar-compact-header">';
13219378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
13387ac9bf3SAtari911        $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>';
13419378907SAtari911        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
13587ac9bf3SAtari911        $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
13619378907SAtari911        $html .= '</div>';
13719378907SAtari911
1381d05cddcSAtari911        // Namespace filter indicator - only show if actively filtering a specific namespace
1391d05cddcSAtari911        if ($namespace && $namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false) {
1401d05cddcSAtari911            $html .= '<div class="calendar-namespace-filter" id="namespace-filter-' . $calId . '">';
1411d05cddcSAtari911            $html .= '<span class="namespace-filter-label">Filtering:</span>';
1421d05cddcSAtari911            $html .= '<span class="namespace-filter-name">' . htmlspecialchars($namespace) . '</span>';
1431d05cddcSAtari911            $html .= '<button class="namespace-filter-clear" onclick="clearNamespaceFilter(\'' . $calId . '\')" title="Clear filter and show all namespaces">✕</button>';
1441d05cddcSAtari911            $html .= '</div>';
1451d05cddcSAtari911        }
1461d05cddcSAtari911
14719378907SAtari911        // Calendar grid
14819378907SAtari911        $html .= '<table class="calendar-compact-grid">';
14919378907SAtari911        $html .= '<thead><tr>';
15019378907SAtari911        $html .= '<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>';
15119378907SAtari911        $html .= '</tr></thead><tbody>';
15219378907SAtari911
15319378907SAtari911        $firstDay = mktime(0, 0, 0, $month, 1, $year);
15419378907SAtari911        $daysInMonth = date('t', $firstDay);
15519378907SAtari911        $dayOfWeek = date('w', $firstDay);
15619378907SAtari911
157e3a9f44cSAtari911        // Build a map of all events with their date ranges for the calendar grid
15887ac9bf3SAtari911        $eventRanges = array();
159e3a9f44cSAtari911        foreach ($events as $dateKey => $dayEvents) {
16087ac9bf3SAtari911            foreach ($dayEvents as $evt) {
16187ac9bf3SAtari911                $eventId = isset($evt['id']) ? $evt['id'] : '';
16287ac9bf3SAtari911                $startDate = $dateKey;
16387ac9bf3SAtari911                $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey;
16487ac9bf3SAtari911
16587ac9bf3SAtari911                // Only process events that touch this month
16687ac9bf3SAtari911                $eventStart = new DateTime($startDate);
16787ac9bf3SAtari911                $eventEnd = new DateTime($endDate);
16887ac9bf3SAtari911                $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month));
16987ac9bf3SAtari911                $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth));
17087ac9bf3SAtari911
17187ac9bf3SAtari911                // Skip if event doesn't overlap with current month
17287ac9bf3SAtari911                if ($eventEnd < $monthStart || $eventStart > $monthEnd) {
17387ac9bf3SAtari911                    continue;
17487ac9bf3SAtari911                }
17587ac9bf3SAtari911
17687ac9bf3SAtari911                // Create entry for each day the event spans
17787ac9bf3SAtari911                $current = clone $eventStart;
17887ac9bf3SAtari911                while ($current <= $eventEnd) {
17987ac9bf3SAtari911                    $currentKey = $current->format('Y-m-d');
18087ac9bf3SAtari911
18187ac9bf3SAtari911                    // Check if this date is in current month
18287ac9bf3SAtari911                    $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey);
18387ac9bf3SAtari911                    if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) {
18487ac9bf3SAtari911                        if (!isset($eventRanges[$currentKey])) {
18587ac9bf3SAtari911                            $eventRanges[$currentKey] = array();
18687ac9bf3SAtari911                        }
18787ac9bf3SAtari911
18887ac9bf3SAtari911                        // Add event with span information
18987ac9bf3SAtari911                        $evt['_span_start'] = $startDate;
19087ac9bf3SAtari911                        $evt['_span_end'] = $endDate;
19187ac9bf3SAtari911                        $evt['_is_first_day'] = ($currentKey === $startDate);
19287ac9bf3SAtari911                        $evt['_is_last_day'] = ($currentKey === $endDate);
19387ac9bf3SAtari911                        $evt['_original_date'] = $dateKey; // Keep track of original date
19487ac9bf3SAtari911
19587ac9bf3SAtari911                        // Check if event continues from previous month or to next month
19687ac9bf3SAtari911                        $evt['_continues_from_prev'] = ($eventStart < $monthStart);
19787ac9bf3SAtari911                        $evt['_continues_to_next'] = ($eventEnd > $monthEnd);
19887ac9bf3SAtari911
19987ac9bf3SAtari911                        $eventRanges[$currentKey][] = $evt;
20087ac9bf3SAtari911                    }
20187ac9bf3SAtari911
20287ac9bf3SAtari911                    $current->modify('+1 day');
20387ac9bf3SAtari911                }
20487ac9bf3SAtari911            }
20587ac9bf3SAtari911        }
20687ac9bf3SAtari911
20719378907SAtari911        $currentDay = 1;
20819378907SAtari911        $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7);
20919378907SAtari911
21019378907SAtari911        for ($row = 0; $row < $rowCount; $row++) {
21119378907SAtari911            $html .= '<tr>';
21219378907SAtari911            for ($col = 0; $col < 7; $col++) {
21319378907SAtari911                if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) {
21419378907SAtari911                    $html .= '<td class="cal-empty"></td>';
21519378907SAtari911                } else {
21619378907SAtari911                    $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay);
21719378907SAtari911                    $isToday = ($dateKey === date('Y-m-d'));
21887ac9bf3SAtari911                    $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]);
21919378907SAtari911
22019378907SAtari911                    $classes = 'cal-day';
22119378907SAtari911                    if ($isToday) $classes .= ' cal-today';
22219378907SAtari911                    if ($hasEvents) $classes .= ' cal-has-events';
22319378907SAtari911
22419378907SAtari911                    $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">';
22519378907SAtari911                    $html .= '<span class="day-num">' . $currentDay . '</span>';
22619378907SAtari911
22719378907SAtari911                    if ($hasEvents) {
22819378907SAtari911                        // Sort events by time (no time first, then by time)
22987ac9bf3SAtari911                        $sortedEvents = $eventRanges[$dateKey];
23019378907SAtari911                        usort($sortedEvents, function($a, $b) {
23119378907SAtari911                            $timeA = isset($a['time']) ? $a['time'] : '';
23219378907SAtari911                            $timeB = isset($b['time']) ? $b['time'] : '';
23319378907SAtari911
23419378907SAtari911                            // Events without time go first
23519378907SAtari911                            if (empty($timeA) && !empty($timeB)) return -1;
23619378907SAtari911                            if (!empty($timeA) && empty($timeB)) return 1;
23719378907SAtari911                            if (empty($timeA) && empty($timeB)) return 0;
23819378907SAtari911
23919378907SAtari911                            // Sort by time
24019378907SAtari911                            return strcmp($timeA, $timeB);
24119378907SAtari911                        });
24219378907SAtari911
24319378907SAtari911                        // Show colored stacked bars for each event
24419378907SAtari911                        $html .= '<div class="event-indicators">';
24519378907SAtari911                        foreach ($sortedEvents as $evt) {
24619378907SAtari911                            $eventId = isset($evt['id']) ? $evt['id'] : '';
24719378907SAtari911                            $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db';
24819378907SAtari911                            $eventTime = isset($evt['time']) ? $evt['time'] : '';
24919378907SAtari911                            $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event';
25087ac9bf3SAtari911                            $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey;
25187ac9bf3SAtari911                            $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true;
25287ac9bf3SAtari911                            $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true;
25319378907SAtari911
25419378907SAtari911                            $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed';
25519378907SAtari911
25687ac9bf3SAtari911                            // Add classes for multi-day spanning
25787ac9bf3SAtari911                            if (!$isFirstDay) $barClass .= ' event-bar-continues';
25887ac9bf3SAtari911                            if (!$isLastDay) $barClass .= ' event-bar-continuing';
25987ac9bf3SAtari911
26019378907SAtari911                            $html .= '<span class="event-bar ' . $barClass . '" ';
26119378907SAtari911                            $html .= 'style="background: ' . $eventColor . ';" ';
26219378907SAtari911                            $html .= 'title="' . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" ';
26387ac9bf3SAtari911                            $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">';
26419378907SAtari911                            $html .= '</span>';
26519378907SAtari911                        }
26619378907SAtari911                        $html .= '</div>';
26719378907SAtari911                    }
26819378907SAtari911
26919378907SAtari911                    $html .= '</td>';
27019378907SAtari911                    $currentDay++;
27119378907SAtari911                }
27219378907SAtari911            }
27319378907SAtari911            $html .= '</tr>';
27419378907SAtari911        }
27519378907SAtari911
27619378907SAtari911        $html .= '</tbody></table>';
27719378907SAtari911        $html .= '</div>'; // End calendar-left
27819378907SAtari911
27919378907SAtari911        // Right side: Event list
28019378907SAtari911        $html .= '<div class="calendar-compact-right">';
28119378907SAtari911        $html .= '<div class="event-list-header">';
28219378907SAtari911        $html .= '<div class="event-list-header-content">';
28319378907SAtari911        $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>';
28419378907SAtari911        if ($namespace) {
28519378907SAtari911            $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>';
28619378907SAtari911        }
28719378907SAtari911        $html .= '</div>';
2881d05cddcSAtari911
2891d05cddcSAtari911        // Search bar in header
2901d05cddcSAtari911        $html .= '<div class="event-search-container-inline">';
2911d05cddcSAtari911        $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder="�� Search..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
2921d05cddcSAtari911        $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
2931d05cddcSAtari911        $html .= '</div>';
2941d05cddcSAtari911
29519378907SAtari911        $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
29619378907SAtari911        $html .= '</div>';
29719378907SAtari911
29819378907SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">';
29919378907SAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace);
30019378907SAtari911        $html .= '</div>';
30119378907SAtari911
30219378907SAtari911        $html .= '</div>'; // End calendar-right
30319378907SAtari911
30419378907SAtari911        // Event dialog
30519378907SAtari911        $html .= $this->renderEventDialog($calId, $namespace);
30619378907SAtari911
30787ac9bf3SAtari911        // Month/Year picker dialog (at container level for proper overlay)
30887ac9bf3SAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace);
30987ac9bf3SAtari911
31019378907SAtari911        $html .= '</div>'; // End container
31119378907SAtari911
31219378907SAtari911        return $html;
31319378907SAtari911    }
31419378907SAtari911
31519378907SAtari911    private function renderEventListContent($events, $calId, $namespace) {
31619378907SAtari911        if (empty($events)) {
31719378907SAtari911            return '<p class="no-events-msg">No events this month</p>';
31819378907SAtari911        }
31919378907SAtari911
3201d05cddcSAtari911        // Check for time conflicts
3211d05cddcSAtari911        $events = $this->checkTimeConflicts($events);
3221d05cddcSAtari911
323e3a9f44cSAtari911        // Sort by date ascending (chronological order - oldest first)
32419378907SAtari911        ksort($events);
32519378907SAtari911
326e3a9f44cSAtari911        // Sort events within each day by time
327e3a9f44cSAtari911        foreach ($events as $dateKey => &$dayEvents) {
328e3a9f44cSAtari911            usort($dayEvents, function($a, $b) {
3291d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
3301d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
3311d05cddcSAtari911
3321d05cddcSAtari911                // All-day events (no time) go to the TOP
3331d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
3341d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
3351d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
3361d05cddcSAtari911
3371d05cddcSAtari911                // Both have times, sort chronologically
338e3a9f44cSAtari911                return strcmp($timeA, $timeB);
339e3a9f44cSAtari911            });
340e3a9f44cSAtari911        }
341e3a9f44cSAtari911        unset($dayEvents); // Break reference
342e3a9f44cSAtari911
343e3a9f44cSAtari911        // Get today's date for comparison
344e3a9f44cSAtari911        $today = date('Y-m-d');
345e3a9f44cSAtari911        $firstFutureEventId = null;
346e3a9f44cSAtari911
3471d05cddcSAtari911        // Helper function to check if event is past (with 15-minute grace period for timed events)
3481d05cddcSAtari911        $isEventPast = function($dateKey, $time) use ($today) {
3491d05cddcSAtari911            // If event is on a past date, it's definitely past
3501d05cddcSAtari911            if ($dateKey < $today) {
3511d05cddcSAtari911                return true;
3521d05cddcSAtari911            }
3531d05cddcSAtari911
3541d05cddcSAtari911            // If event is on a future date, it's definitely not past
3551d05cddcSAtari911            if ($dateKey > $today) {
3561d05cddcSAtari911                return false;
3571d05cddcSAtari911            }
3581d05cddcSAtari911
3591d05cddcSAtari911            // Event is today - check time with grace period
3601d05cddcSAtari911            if ($time && $time !== '') {
3611d05cddcSAtari911                try {
3621d05cddcSAtari911                    $currentDateTime = new DateTime();
3631d05cddcSAtari911                    $eventDateTime = new DateTime($dateKey . ' ' . $time);
3641d05cddcSAtari911
3651d05cddcSAtari911                    // Add 15-minute grace period
3661d05cddcSAtari911                    $eventDateTime->modify('+15 minutes');
3671d05cddcSAtari911
3681d05cddcSAtari911                    // Event is past if current time > event time + 15 minutes
3691d05cddcSAtari911                    return $currentDateTime > $eventDateTime;
3701d05cddcSAtari911                } catch (Exception $e) {
3711d05cddcSAtari911                    // If time parsing fails, fall back to date-only comparison
3721d05cddcSAtari911                    return false;
3731d05cddcSAtari911                }
3741d05cddcSAtari911            }
3751d05cddcSAtari911
3761d05cddcSAtari911            // No time specified for today's event, treat as future
3771d05cddcSAtari911            return false;
3781d05cddcSAtari911        };
3791d05cddcSAtari911
3801d05cddcSAtari911        // Build HTML for each event - separate past/completed from future
3811d05cddcSAtari911        $pastHtml = '';
3821d05cddcSAtari911        $futureHtml = '';
3831d05cddcSAtari911        $pastCount = 0;
384e3a9f44cSAtari911
38519378907SAtari911        foreach ($events as $dateKey => $dayEvents) {
386e3a9f44cSAtari911
38719378907SAtari911            foreach ($dayEvents as $event) {
388e3a9f44cSAtari911                // Track first future/today event for auto-scroll
389e3a9f44cSAtari911                if (!$firstFutureEventId && $dateKey >= $today) {
390e3a9f44cSAtari911                    $firstFutureEventId = isset($event['id']) ? $event['id'] : '';
391e3a9f44cSAtari911                }
39219378907SAtari911                $eventId = isset($event['id']) ? $event['id'] : '';
39319378907SAtari911                $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
3941d05cddcSAtari911                $timeRaw = isset($event['time']) ? $event['time'] : '';
3951d05cddcSAtari911                $time = htmlspecialchars($timeRaw);
3961d05cddcSAtari911                $endTime = isset($event['endTime']) ? htmlspecialchars($event['endTime']) : '';
39719378907SAtari911                $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db';
39819378907SAtari911                $description = isset($event['description']) ? $event['description'] : '';
39919378907SAtari911                $isTask = isset($event['isTask']) ? $event['isTask'] : false;
40019378907SAtari911                $completed = isset($event['completed']) ? $event['completed'] : false;
40119378907SAtari911                $endDate = isset($event['endDate']) ? $event['endDate'] : '';
40219378907SAtari911
4031d05cddcSAtari911                // Use helper function to determine if event is past (with grace period)
4041d05cddcSAtari911                $isPast = $isEventPast($dateKey, $timeRaw);
4051d05cddcSAtari911                $isToday = $dateKey === $today;
4061d05cddcSAtari911
4071d05cddcSAtari911                // Check if event should be in past section
4081d05cddcSAtari911                // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past
4091d05cddcSAtari911                $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed;
4101d05cddcSAtari911                if ($isPastOrCompleted) {
4111d05cddcSAtari911                    $pastCount++;
4121d05cddcSAtari911                }
4131d05cddcSAtari911
4141d05cddcSAtari911                // Determine if task is past due (past date, is task, not completed)
4151d05cddcSAtari911                $isPastDue = $isPast && $isTask && !$completed;
4161d05cddcSAtari911
41719378907SAtari911                // Process description for wiki syntax, HTML, images, and links
41819378907SAtari911                $renderedDescription = $this->renderDescription($description);
41919378907SAtari911
4201d05cddcSAtari911                // Convert to 12-hour format and handle time ranges
42119378907SAtari911                $displayTime = '';
42219378907SAtari911                if ($time) {
42319378907SAtari911                    $timeObj = DateTime::createFromFormat('H:i', $time);
42419378907SAtari911                    if ($timeObj) {
42519378907SAtari911                        $displayTime = $timeObj->format('g:i A');
4261d05cddcSAtari911
4271d05cddcSAtari911                        // Add end time if present and different from start time
4281d05cddcSAtari911                        if ($endTime && $endTime !== $time) {
4291d05cddcSAtari911                            $endTimeObj = DateTime::createFromFormat('H:i', $endTime);
4301d05cddcSAtari911                            if ($endTimeObj) {
4311d05cddcSAtari911                                $displayTime .= ' - ' . $endTimeObj->format('g:i A');
4321d05cddcSAtari911                            }
4331d05cddcSAtari911                        }
43419378907SAtari911                    } else {
43519378907SAtari911                        $displayTime = $time;
43619378907SAtari911                    }
43719378907SAtari911                }
43819378907SAtari911
43987ac9bf3SAtari911                // Format date display with day of week
440e3a9f44cSAtari911                // Use originalStartDate if this is a multi-month event continuation
441e3a9f44cSAtari911                $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey;
442e3a9f44cSAtari911                $dateObj = new DateTime($displayDateKey);
44387ac9bf3SAtari911                $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24"
44419378907SAtari911
44519378907SAtari911                // Multi-day indicator
44619378907SAtari911                $multiDay = '';
447e3a9f44cSAtari911                if ($endDate && $endDate !== $displayDateKey) {
44819378907SAtari911                    $endObj = new DateTime($endDate);
44987ac9bf3SAtari911                    $multiDay = ' → ' . $endObj->format('D, M j');
45019378907SAtari911                }
45119378907SAtari911
45219378907SAtari911                $completedClass = $completed ? ' event-completed' : '';
4531d05cddcSAtari911                // Don't grey out past due tasks - they need attention!
4541d05cddcSAtari911                $pastClass = ($isPast && !$isPastDue) ? ' event-past' : '';
4551d05cddcSAtari911                $pastDueClass = $isPastDue ? ' event-pastdue' : '';
456e3a9f44cSAtari911                $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : '';
45719378907SAtari911
4581d05cddcSAtari911                $eventHtml = '<div class="event-compact-item' . $completedClass . $pastClass . $pastDueClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';"' . $firstFutureAttr . '>';
45919378907SAtari911
4601d05cddcSAtari911                $eventHtml .= '<div class="event-info">';
4611d05cddcSAtari911                $eventHtml .= '<div class="event-title-row">';
4621d05cddcSAtari911                $eventHtml .= '<span class="event-title-compact">' . $title . '</span>';
4631d05cddcSAtari911                $eventHtml .= '</div>';
46419378907SAtari911
465e3a9f44cSAtari911                // For past events, hide meta and description (collapsed)
4661d05cddcSAtari911                // EXCEPTION: Past due tasks should show their details
4671d05cddcSAtari911                if (!$isPast || $isPastDue) {
4681d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact">';
4691d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
47019378907SAtari911                    if ($displayTime) {
4711d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
47219378907SAtari911                    }
4731d05cddcSAtari911                    // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks
4741d05cddcSAtari911                    if ($isPastDue) {
4751d05cddcSAtari911                        $eventHtml .= ' <span class="event-pastdue-badge">PAST DUE</span>';
4761d05cddcSAtari911                    } elseif ($isToday) {
4771d05cddcSAtari911                        $eventHtml .= ' <span class="event-today-badge">TODAY</span>';
478e3a9f44cSAtari911                    }
4791d05cddcSAtari911                    // Add namespace badge - ALWAYS show if event has a namespace
480e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
481e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
482e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility
483e3a9f44cSAtari911                    }
4841d05cddcSAtari911                    // Show badge if namespace exists and is not empty
4851d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
4861d05cddcSAtari911                        $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>';
487e3a9f44cSAtari911                    }
4881d05cddcSAtari911
4891d05cddcSAtari911                    // Add conflict warning if event has time conflicts
4901d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
4911d05cddcSAtari911                        $conflictList = [];
4921d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
4931d05cddcSAtari911                            $conflictText = htmlspecialchars($conflict['title']);
4941d05cddcSAtari911                            if (!empty($conflict['time'])) {
4951d05cddcSAtari911                                // Format time range
4961d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
4971d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
4981d05cddcSAtari911
4991d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
5001d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
5011d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
5021d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
5031d05cddcSAtari911                                } else {
5041d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
5051d05cddcSAtari911                                }
5061d05cddcSAtari911                            }
5071d05cddcSAtari911                            $conflictList[] = $conflictText;
5081d05cddcSAtari911                        }
5091d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
5101d05cddcSAtari911                        $conflictJson = htmlspecialchars(json_encode($conflictList), ENT_QUOTES, 'UTF-8');
5111d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
5121d05cddcSAtari911                    }
5131d05cddcSAtari911
5141d05cddcSAtari911                    $eventHtml .= '</span>';
5151d05cddcSAtari911                    $eventHtml .= '</div>';
51619378907SAtari911
51719378907SAtari911                    if ($description) {
5181d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>';
5191d05cddcSAtari911                    }
5201d05cddcSAtari911                } else {
5211d05cddcSAtari911                    // Past events: render with display:none for click-to-expand
5221d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact" style="display:none;">';
5231d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
5241d05cddcSAtari911                    if ($displayTime) {
5251d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
5261d05cddcSAtari911                    }
5271d05cddcSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
5281d05cddcSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
5291d05cddcSAtari911                        $eventNamespace = $event['_namespace'];
5301d05cddcSAtari911                    }
5311d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
5321d05cddcSAtari911                        $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>';
5331d05cddcSAtari911                    }
5341d05cddcSAtari911
5351d05cddcSAtari911                    // Add conflict warning if event has time conflicts
5361d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
5371d05cddcSAtari911                        $conflictList = [];
5381d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
5391d05cddcSAtari911                            $conflictText = htmlspecialchars($conflict['title']);
5401d05cddcSAtari911                            if (!empty($conflict['time'])) {
5411d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
5421d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
5431d05cddcSAtari911
5441d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
5451d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
5461d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
5471d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
5481d05cddcSAtari911                                } else {
5491d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
5501d05cddcSAtari911                                }
5511d05cddcSAtari911                            }
5521d05cddcSAtari911                            $conflictList[] = $conflictText;
5531d05cddcSAtari911                        }
5541d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
5551d05cddcSAtari911                        $conflictJson = htmlspecialchars(json_encode($conflictList), ENT_QUOTES, 'UTF-8');
5561d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
5571d05cddcSAtari911                    }
5581d05cddcSAtari911
5591d05cddcSAtari911                    $eventHtml .= '</span>';
5601d05cddcSAtari911                    $eventHtml .= '</div>';
5611d05cddcSAtari911
5621d05cddcSAtari911                    if ($description) {
5631d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>';
56419378907SAtari911                    }
565e3a9f44cSAtari911                }
56619378907SAtari911
5671d05cddcSAtari911                $eventHtml .= '</div>'; // event-info
56819378907SAtari911
569e3a9f44cSAtari911                // Use stored namespace from event, fallback to passed namespace
570e3a9f44cSAtari911                $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace;
571e3a9f44cSAtari911
5721d05cddcSAtari911                $eventHtml .= '<div class="event-actions-compact">';
5731d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">��️</button>';
5741d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>';
5751d05cddcSAtari911                $eventHtml .= '</div>';
57619378907SAtari911
57719378907SAtari911                // Checkbox for tasks - ON THE FAR RIGHT
57819378907SAtari911                if ($isTask) {
57919378907SAtari911                    $checked = $completed ? 'checked' : '';
5801d05cddcSAtari911                    $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">';
58119378907SAtari911                }
58219378907SAtari911
5831d05cddcSAtari911                $eventHtml .= '</div>';
5841d05cddcSAtari911
5851d05cddcSAtari911                // Add to appropriate section
5861d05cddcSAtari911                if ($isPastOrCompleted) {
5871d05cddcSAtari911                    $pastHtml .= $eventHtml;
5881d05cddcSAtari911                } else {
5891d05cddcSAtari911                    $futureHtml .= $eventHtml;
5901d05cddcSAtari911                }
5911d05cddcSAtari911            }
5921d05cddcSAtari911        }
5931d05cddcSAtari911
5941d05cddcSAtari911        // Build final HTML with collapsible past events section
5951d05cddcSAtari911        $html = '';
5961d05cddcSAtari911
5971d05cddcSAtari911        // Add collapsible past events section if any exist
5981d05cddcSAtari911        if ($pastCount > 0) {
5991d05cddcSAtari911            $html .= '<div class="past-events-section">';
6001d05cddcSAtari911            $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">';
6011d05cddcSAtari911            $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> ';
6021d05cddcSAtari911            $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>';
60319378907SAtari911            $html .= '</div>';
6041d05cddcSAtari911            $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">';
6051d05cddcSAtari911            $html .= $pastHtml;
6061d05cddcSAtari911            $html .= '</div>';
6071d05cddcSAtari911            $html .= '</div>';
6081d05cddcSAtari911        }
609e3a9f44cSAtari911
6101d05cddcSAtari911        // Add future events
6111d05cddcSAtari911        $html .= $futureHtml;
61219378907SAtari911
61319378907SAtari911        return $html;
61419378907SAtari911    }
61519378907SAtari911
6161d05cddcSAtari911    /**
6171d05cddcSAtari911     * Check for time conflicts between events
6181d05cddcSAtari911     */
6191d05cddcSAtari911    private function checkTimeConflicts($events) {
6201d05cddcSAtari911        // Group events by date
6211d05cddcSAtari911        $eventsByDate = [];
6221d05cddcSAtari911        foreach ($events as $date => $dateEvents) {
6231d05cddcSAtari911            if (!is_array($dateEvents)) continue;
6241d05cddcSAtari911
6251d05cddcSAtari911            foreach ($dateEvents as $evt) {
6261d05cddcSAtari911                if (empty($evt['time'])) continue; // Skip all-day events
6271d05cddcSAtari911
6281d05cddcSAtari911                if (!isset($eventsByDate[$date])) {
6291d05cddcSAtari911                    $eventsByDate[$date] = [];
6301d05cddcSAtari911                }
6311d05cddcSAtari911                $eventsByDate[$date][] = $evt;
6321d05cddcSAtari911            }
6331d05cddcSAtari911        }
6341d05cddcSAtari911
6351d05cddcSAtari911        // Check for overlaps on each date
6361d05cddcSAtari911        foreach ($eventsByDate as $date => $dateEvents) {
6371d05cddcSAtari911            for ($i = 0; $i < count($dateEvents); $i++) {
6381d05cddcSAtari911                for ($j = $i + 1; $j < count($dateEvents); $j++) {
6391d05cddcSAtari911                    if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) {
6401d05cddcSAtari911                        // Mark both events as conflicting
6411d05cddcSAtari911                        $dateEvents[$i]['hasConflict'] = true;
6421d05cddcSAtari911                        $dateEvents[$j]['hasConflict'] = true;
6431d05cddcSAtari911
6441d05cddcSAtari911                        // Store conflict info
6451d05cddcSAtari911                        if (!isset($dateEvents[$i]['conflictsWith'])) {
6461d05cddcSAtari911                            $dateEvents[$i]['conflictsWith'] = [];
6471d05cddcSAtari911                        }
6481d05cddcSAtari911                        if (!isset($dateEvents[$j]['conflictsWith'])) {
6491d05cddcSAtari911                            $dateEvents[$j]['conflictsWith'] = [];
6501d05cddcSAtari911                        }
6511d05cddcSAtari911
6521d05cddcSAtari911                        $dateEvents[$i]['conflictsWith'][] = [
6531d05cddcSAtari911                            'id' => $dateEvents[$j]['id'],
6541d05cddcSAtari911                            'title' => $dateEvents[$j]['title'],
6551d05cddcSAtari911                            'time' => $dateEvents[$j]['time'],
6561d05cddcSAtari911                            'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : ''
6571d05cddcSAtari911                        ];
6581d05cddcSAtari911
6591d05cddcSAtari911                        $dateEvents[$j]['conflictsWith'][] = [
6601d05cddcSAtari911                            'id' => $dateEvents[$i]['id'],
6611d05cddcSAtari911                            'title' => $dateEvents[$i]['title'],
6621d05cddcSAtari911                            'time' => $dateEvents[$i]['time'],
6631d05cddcSAtari911                            'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : ''
6641d05cddcSAtari911                        ];
6651d05cddcSAtari911                    }
6661d05cddcSAtari911                }
6671d05cddcSAtari911            }
6681d05cddcSAtari911
6691d05cddcSAtari911            // Update the events array with conflict information
6701d05cddcSAtari911            foreach ($events[$date] as &$evt) {
6711d05cddcSAtari911                foreach ($dateEvents as $checkedEvt) {
6721d05cddcSAtari911                    if ($evt['id'] === $checkedEvt['id']) {
6731d05cddcSAtari911                        if (isset($checkedEvt['hasConflict'])) {
6741d05cddcSAtari911                            $evt['hasConflict'] = $checkedEvt['hasConflict'];
6751d05cddcSAtari911                        }
6761d05cddcSAtari911                        if (isset($checkedEvt['conflictsWith'])) {
6771d05cddcSAtari911                            $evt['conflictsWith'] = $checkedEvt['conflictsWith'];
6781d05cddcSAtari911                        }
6791d05cddcSAtari911                        break;
6801d05cddcSAtari911                    }
6811d05cddcSAtari911                }
6821d05cddcSAtari911            }
6831d05cddcSAtari911        }
6841d05cddcSAtari911
6851d05cddcSAtari911        return $events;
6861d05cddcSAtari911    }
6871d05cddcSAtari911
6881d05cddcSAtari911    /**
6891d05cddcSAtari911     * Check if two events overlap in time
6901d05cddcSAtari911     */
6911d05cddcSAtari911    private function eventsOverlap($evt1, $evt2) {
6921d05cddcSAtari911        if (empty($evt1['time']) || empty($evt2['time'])) {
6931d05cddcSAtari911            return false; // All-day events don't conflict
6941d05cddcSAtari911        }
6951d05cddcSAtari911
6961d05cddcSAtari911        $start1 = $evt1['time'];
6971d05cddcSAtari911        $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time'];
6981d05cddcSAtari911
6991d05cddcSAtari911        $start2 = $evt2['time'];
7001d05cddcSAtari911        $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time'];
7011d05cddcSAtari911
7021d05cddcSAtari911        // Convert to minutes for easier comparison
7031d05cddcSAtari911        $start1Mins = $this->timeToMinutes($start1);
7041d05cddcSAtari911        $end1Mins = $this->timeToMinutes($end1);
7051d05cddcSAtari911        $start2Mins = $this->timeToMinutes($start2);
7061d05cddcSAtari911        $end2Mins = $this->timeToMinutes($end2);
7071d05cddcSAtari911
7081d05cddcSAtari911        // Check for overlap: start1 < end2 AND start2 < end1
7091d05cddcSAtari911        return $start1Mins < $end2Mins && $start2Mins < $end1Mins;
7101d05cddcSAtari911    }
7111d05cddcSAtari911
7121d05cddcSAtari911    /**
7131d05cddcSAtari911     * Convert HH:MM time to minutes since midnight
7141d05cddcSAtari911     */
7151d05cddcSAtari911    private function timeToMinutes($timeStr) {
7161d05cddcSAtari911        $parts = explode(':', $timeStr);
7171d05cddcSAtari911        if (count($parts) !== 2) return 0;
7181d05cddcSAtari911
7191d05cddcSAtari911        return (int)$parts[0] * 60 + (int)$parts[1];
7201d05cddcSAtari911    }
7211d05cddcSAtari911
72219378907SAtari911    private function renderEventPanelOnly($data) {
72319378907SAtari911        $year = (int)$data['year'];
72419378907SAtari911        $month = (int)$data['month'];
72519378907SAtari911        $namespace = $data['namespace'];
72687ac9bf3SAtari911        $height = isset($data['height']) ? $data['height'] : '400px';
72787ac9bf3SAtari911
72887ac9bf3SAtari911        // Validate height format (must be px, em, rem, vh, or %)
72987ac9bf3SAtari911        if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) {
73087ac9bf3SAtari911            $height = '400px'; // Default fallback
73187ac9bf3SAtari911        }
73219378907SAtari911
733e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
734e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
735e3a9f44cSAtari911
736e3a9f44cSAtari911        if ($isMultiNamespace) {
737e3a9f44cSAtari911            $events = $this->loadEventsMultiNamespace($namespace, $year, $month);
738e3a9f44cSAtari911        } else {
73919378907SAtari911            $events = $this->loadEvents($namespace, $year, $month);
740e3a9f44cSAtari911        }
74119378907SAtari911        $calId = 'panel_' . md5(serialize($data) . microtime());
74219378907SAtari911
74319378907SAtari911        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
74419378907SAtari911
74519378907SAtari911        $prevMonth = $month - 1;
74619378907SAtari911        $prevYear = $year;
74719378907SAtari911        if ($prevMonth < 1) {
74819378907SAtari911            $prevMonth = 12;
74919378907SAtari911            $prevYear--;
75019378907SAtari911        }
75119378907SAtari911
75219378907SAtari911        $nextMonth = $month + 1;
75319378907SAtari911        $nextYear = $year;
75419378907SAtari911        if ($nextMonth > 12) {
75519378907SAtari911            $nextMonth = 1;
75619378907SAtari911            $nextYear++;
75719378907SAtari911        }
75819378907SAtari911
7591d05cddcSAtari911        $html = '<div class="event-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '">';
76019378907SAtari911
7611d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
7621d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
7631d05cddcSAtari911
7641d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
7651d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
7661d05cddcSAtari911
7671d05cddcSAtari911        // Compact two-row header designed for ~500px width
7681d05cddcSAtari911        $html .= '<div class="panel-header-compact">';
7691d05cddcSAtari911
7701d05cddcSAtari911        // Row 1: Navigation and title
7711d05cddcSAtari911        $html .= '<div class="panel-header-row-1">';
7721d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
7731d05cddcSAtari911
7741d05cddcSAtari911        // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events")
7751d05cddcSAtari911        $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year));
7761d05cddcSAtari911        $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>';
7771d05cddcSAtari911
7781d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
7791d05cddcSAtari911
7801d05cddcSAtari911        // Namespace badge (if applicable)
78187ac9bf3SAtari911        if ($namespace) {
782e3a9f44cSAtari911            if ($isMultiNamespace) {
783e3a9f44cSAtari911                if (strpos($namespace, '*') !== false) {
7841d05cddcSAtari911                    $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>';
785e3a9f44cSAtari911                } else {
786e3a9f44cSAtari911                    $namespaceList = array_map('trim', explode(';', $namespace));
7871d05cddcSAtari911                    $nsCount = count($namespaceList);
7881d05cddcSAtari911                    $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars(implode(', ', $namespaceList)) . '">' . $nsCount . ' NS</span>';
789e3a9f44cSAtari911                }
790e3a9f44cSAtari911            } else {
7911d05cddcSAtari911                $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false);
7921d05cddcSAtari911                if ($isFiltering) {
7931d05cddcSAtari911                    $html .= '<span class="panel-ns-badge filter-on" title="Filtering by ' . htmlspecialchars($namespace) . ' - click to clear" onclick="clearNamespaceFilterPanel(\'' . $calId . '\')">' . htmlspecialchars($namespace) . ' ✕</span>';
7941d05cddcSAtari911                } else {
7951d05cddcSAtari911                    $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>';
79687ac9bf3SAtari911                }
797e3a9f44cSAtari911            }
7981d05cddcSAtari911        }
7991d05cddcSAtari911
8001d05cddcSAtari911        $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
80119378907SAtari911        $html .= '</div>';
80219378907SAtari911
8031d05cddcSAtari911        // Row 2: Search and add button
8041d05cddcSAtari911        $html .= '<div class="panel-header-row-2">';
8051d05cddcSAtari911        $html .= '<div class="panel-search-box">';
8061d05cddcSAtari911        $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="Search events..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
8071d05cddcSAtari911        $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
8081d05cddcSAtari911        $html .= '</div>';
8091d05cddcSAtari911        $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
8101d05cddcSAtari911        $html .= '</div>';
8111d05cddcSAtari911
81219378907SAtari911        $html .= '</div>';
81319378907SAtari911
81487ac9bf3SAtari911        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">';
81519378907SAtari911        $html .= $this->renderEventListContent($events, $calId, $namespace);
81619378907SAtari911        $html .= '</div>';
81719378907SAtari911
81819378907SAtari911        $html .= $this->renderEventDialog($calId, $namespace);
81919378907SAtari911
82087ac9bf3SAtari911        // Month/Year picker for event panel
82187ac9bf3SAtari911        $html .= $this->renderMonthPicker($calId, $year, $month, $namespace);
82287ac9bf3SAtari911
82319378907SAtari911        $html .= '</div>';
82419378907SAtari911
82519378907SAtari911        return $html;
82619378907SAtari911    }
82719378907SAtari911
82819378907SAtari911    private function renderStandaloneEventList($data) {
82919378907SAtari911        $namespace = $data['namespace'];
8301d05cddcSAtari911        // If no namespace specified, show all namespaces
8311d05cddcSAtari911        if (empty($namespace)) {
8321d05cddcSAtari911            $namespace = '*';
8331d05cddcSAtari911        }
83419378907SAtari911        $daterange = $data['daterange'];
83519378907SAtari911        $date = $data['date'];
836e3a9f44cSAtari911        $range = isset($data['range']) ? strtolower($data['range']) : '';
83787ac9bf3SAtari911        $today = isset($data['today']) ? true : false;
838e3a9f44cSAtari911        $sidebar = isset($data['sidebar']) ? true : false;
8391d05cddcSAtari911        $showchecked = isset($data['showchecked']) ? true : false; // New parameter
8401d05cddcSAtari911        $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header
84119378907SAtari911
842e3a9f44cSAtari911        // Handle "range" parameter - day, week, or month
843e3a9f44cSAtari911        if ($range === 'day') {
8441d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
84587ac9bf3SAtari911            $endDate = date('Y-m-d');
846e3a9f44cSAtari911            $headerText = 'Today';
847e3a9f44cSAtari911        } elseif ($range === 'week') {
8481d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
8491d05cddcSAtari911            $endDateTime = new DateTime();
850e3a9f44cSAtari911            $endDateTime->modify('+7 days');
851e3a9f44cSAtari911            $endDate = $endDateTime->format('Y-m-d');
852e3a9f44cSAtari911            $headerText = 'This Week';
853e3a9f44cSAtari911        } elseif ($range === 'month') {
8541d05cddcSAtari911            $startDate = date('Y-m-01', strtotime('-1 month')); // Include previous month for past due tasks
855e3a9f44cSAtari911            $endDate = date('Y-m-t'); // Last of current month
8561d05cddcSAtari911            $dt = new DateTime();
857e3a9f44cSAtari911            $headerText = $dt->format('F Y');
858e3a9f44cSAtari911        } elseif ($sidebar) {
8591d05cddcSAtari911            // NEW: Sidebar widget - load current week's events
8601d05cddcSAtari911            $weekStart = date('Y-m-d', strtotime('monday this week'));
8611d05cddcSAtari911            $weekEnd = date('Y-m-d', strtotime('sunday this week'));
8621d05cddcSAtari911
8631d05cddcSAtari911            // Load events for the entire week
8641d05cddcSAtari911            $start = new DateTime($weekStart);
8651d05cddcSAtari911            $end = new DateTime($weekEnd);
8661d05cddcSAtari911            $end->modify('+1 day'); // DatePeriod excludes end date
8671d05cddcSAtari911            $interval = new DateInterval('P1D');
8681d05cddcSAtari911            $period = new DatePeriod($start, $interval, $end);
8691d05cddcSAtari911
8701d05cddcSAtari911            $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
8711d05cddcSAtari911            $allEvents = [];
8721d05cddcSAtari911            $loadedMonths = [];
8731d05cddcSAtari911
8741d05cddcSAtari911            foreach ($period as $dt) {
8751d05cddcSAtari911                $year = (int)$dt->format('Y');
8761d05cddcSAtari911                $month = (int)$dt->format('n');
8771d05cddcSAtari911                $dateKey = $dt->format('Y-m-d');
8781d05cddcSAtari911
8791d05cddcSAtari911                $monthKey = $year . '-' . $month . '-' . $namespace;
8801d05cddcSAtari911
8811d05cddcSAtari911                if (!isset($loadedMonths[$monthKey])) {
8821d05cddcSAtari911                    if ($isMultiNamespace) {
8831d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
8841d05cddcSAtari911                    } else {
8851d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
8861d05cddcSAtari911                    }
8871d05cddcSAtari911                }
8881d05cddcSAtari911
8891d05cddcSAtari911                $monthEvents = $loadedMonths[$monthKey];
8901d05cddcSAtari911
8911d05cddcSAtari911                if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
8921d05cddcSAtari911                    $allEvents[$dateKey] = $monthEvents[$dateKey];
8931d05cddcSAtari911                }
8941d05cddcSAtari911            }
8951d05cddcSAtari911
8961d05cddcSAtari911            // Apply time conflict detection
8971d05cddcSAtari911            $allEvents = $this->checkTimeConflicts($allEvents);
8981d05cddcSAtari911
8991d05cddcSAtari911            $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8);
9001d05cddcSAtari911
9011d05cddcSAtari911            // Render sidebar widget and return immediately
9021d05cddcSAtari911            return $this->renderSidebarWidget($allEvents, $namespace, $calId);
903e3a9f44cSAtari911        } elseif ($today) {
904e3a9f44cSAtari911            $startDate = date('Y-m-d');
905e3a9f44cSAtari911            $endDate = date('Y-m-d');
906e3a9f44cSAtari911            $headerText = 'Today';
90787ac9bf3SAtari911        } elseif ($daterange) {
90819378907SAtari911            list($startDate, $endDate) = explode(':', $daterange);
909e3a9f44cSAtari911            $start = new DateTime($startDate);
910e3a9f44cSAtari911            $end = new DateTime($endDate);
911e3a9f44cSAtari911            $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y');
91219378907SAtari911        } elseif ($date) {
91319378907SAtari911            $startDate = $date;
91419378907SAtari911            $endDate = $date;
915e3a9f44cSAtari911            $dt = new DateTime($date);
916e3a9f44cSAtari911            $headerText = $dt->format('l, F j, Y');
91719378907SAtari911        } else {
91819378907SAtari911            $startDate = date('Y-m-01');
91919378907SAtari911            $endDate = date('Y-m-t');
920e3a9f44cSAtari911            $dt = new DateTime($startDate);
921e3a9f44cSAtari911            $headerText = $dt->format('F Y');
92219378907SAtari911        }
92319378907SAtari911
924e3a9f44cSAtari911        // Load all events in date range
92519378907SAtari911        $allEvents = array();
92619378907SAtari911        $start = new DateTime($startDate);
92719378907SAtari911        $end = new DateTime($endDate);
92819378907SAtari911        $end->modify('+1 day');
92919378907SAtari911
93019378907SAtari911        $interval = new DateInterval('P1D');
93119378907SAtari911        $period = new DatePeriod($start, $interval, $end);
93219378907SAtari911
933e3a9f44cSAtari911        // Check if multiple namespaces or wildcard specified
934e3a9f44cSAtari911        $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
935e3a9f44cSAtari911
93619378907SAtari911        static $loadedMonths = array();
93719378907SAtari911
93819378907SAtari911        foreach ($period as $dt) {
93919378907SAtari911            $year = (int)$dt->format('Y');
94019378907SAtari911            $month = (int)$dt->format('n');
94119378907SAtari911            $dateKey = $dt->format('Y-m-d');
94219378907SAtari911
943e3a9f44cSAtari911            $monthKey = $year . '-' . $month . '-' . $namespace;
94419378907SAtari911
94519378907SAtari911            if (!isset($loadedMonths[$monthKey])) {
946e3a9f44cSAtari911                if ($isMultiNamespace) {
947e3a9f44cSAtari911                    $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
948e3a9f44cSAtari911                } else {
94919378907SAtari911                    $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
95019378907SAtari911                }
951e3a9f44cSAtari911            }
95219378907SAtari911
95319378907SAtari911            $monthEvents = $loadedMonths[$monthKey];
95419378907SAtari911
95519378907SAtari911            if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
95619378907SAtari911                $allEvents[$dateKey] = $monthEvents[$dateKey];
95719378907SAtari911            }
95819378907SAtari911        }
95919378907SAtari911
9601d05cddcSAtari911        // Sort events by date (already sorted by dateKey), then by time within each day
9611d05cddcSAtari911        foreach ($allEvents as $dateKey => &$dayEvents) {
9621d05cddcSAtari911            usort($dayEvents, function($a, $b) {
9631d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
9641d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
9651d05cddcSAtari911
9661d05cddcSAtari911                // All-day events (no time) go to the TOP
9671d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
9681d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
9691d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
9701d05cddcSAtari911
9711d05cddcSAtari911                // Both have times, sort chronologically
9721d05cddcSAtari911                return strcmp($timeA, $timeB);
9731d05cddcSAtari911            });
9741d05cddcSAtari911        }
9751d05cddcSAtari911        unset($dayEvents); // Break reference
9761d05cddcSAtari911
977e3a9f44cSAtari911        // Simple 2-line display widget
9781d05cddcSAtari911        $calId = 'eventlist_' . uniqid();
9791d05cddcSAtari911        $html = '<div class="eventlist-simple" id="' . $calId . '">';
9801d05cddcSAtari911
9811d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
9821d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
9831d05cddcSAtari911
9841d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
9851d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
9861d05cddcSAtari911
9871d05cddcSAtari911        // Add compact header with date and clock for "today" mode (unless noheader is set)
9881d05cddcSAtari911        if ($today && !empty($allEvents) && !$noheader) {
9891d05cddcSAtari911            $todayDate = new DateTime();
9901d05cddcSAtari911            $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026"
9911d05cddcSAtari911            $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM"
9921d05cddcSAtari911
9931d05cddcSAtari911            $html .= '<div class="eventlist-today-header">';
9941d05cddcSAtari911            $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>';
9951d05cddcSAtari911            $html .= '<div class="eventlist-bottom-info">';
9961d05cddcSAtari911            $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '">--°</span></span>';
9971d05cddcSAtari911            $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>';
9981d05cddcSAtari911            $html .= '</div>';
9991d05cddcSAtari911
10001d05cddcSAtari911            // Three CPU/Memory bars (all update live)
10011d05cddcSAtari911            $html .= '<div class="eventlist-stats-container">';
10021d05cddcSAtari911
10031d05cddcSAtari911            // 5-minute load average (green, updates every 2 seconds)
10041d05cddcSAtari911            $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $calId . '(\'green\')" onmouseout="hideTooltip_' . $calId . '(\'green\')">';
10051d05cddcSAtari911            $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>';
10061d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
10071d05cddcSAtari911            $html .= '</div>';
10081d05cddcSAtari911
10091d05cddcSAtari911            // Real-time CPU (purple, updates with 5-sec average)
10101d05cddcSAtari911            $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $calId . '(\'purple\')" onmouseout="hideTooltip_' . $calId . '(\'purple\')">';
10111d05cddcSAtari911            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>';
10121d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
10131d05cddcSAtari911            $html .= '</div>';
10141d05cddcSAtari911
10151d05cddcSAtari911            // Real-time Memory (orange, updates)
10161d05cddcSAtari911            $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $calId . '(\'orange\')" onmouseout="hideTooltip_' . $calId . '(\'orange\')">';
10171d05cddcSAtari911            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>';
10181d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
10191d05cddcSAtari911            $html .= '</div>';
10201d05cddcSAtari911
10211d05cddcSAtari911            $html .= '</div>';
10221d05cddcSAtari911            $html .= '</div>';
10231d05cddcSAtari911
10241d05cddcSAtari911            // Add JavaScript to update clock and weather
10251d05cddcSAtari911            $html .= '<script>
10261d05cddcSAtari911(function() {
10271d05cddcSAtari911    // Update clock every second
10281d05cddcSAtari911    function updateClock() {
10291d05cddcSAtari911        const now = new Date();
10301d05cddcSAtari911        let hours = now.getHours();
10311d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
10321d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
10331d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
10341d05cddcSAtari911        hours = hours % 12 || 12;
10351d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
10361d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
10371d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
10381d05cddcSAtari911    }
10391d05cddcSAtari911    setInterval(updateClock, 1000);
10401d05cddcSAtari911
10411d05cddcSAtari911    // Fetch weather (geolocation-based)
10421d05cddcSAtari911    function updateWeather() {
10431d05cddcSAtari911        if ("geolocation" in navigator) {
10441d05cddcSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
10451d05cddcSAtari911                const lat = position.coords.latitude;
10461d05cddcSAtari911                const lon = position.coords.longitude;
10471d05cddcSAtari911
10481d05cddcSAtari911                // Use Open-Meteo API (free, no key required)
10491d05cddcSAtari911                fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&temperature_unit=fahrenheit`)
10501d05cddcSAtari911                    .then(response => response.json())
10511d05cddcSAtari911                    .then(data => {
10521d05cddcSAtari911                        if (data.current_weather) {
10531d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
10541d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
10551d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
10561d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
10571d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
10581d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
10591d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
10601d05cddcSAtari911                        }
10611d05cddcSAtari911                    })
10621d05cddcSAtari911                    .catch(error => {
10631d05cddcSAtari911                        console.log("Weather fetch error:", error);
10641d05cddcSAtari911                    });
10651d05cddcSAtari911            }, function(error) {
10661d05cddcSAtari911                // If geolocation fails, use Sacramento as default
10671d05cddcSAtari911                fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944&current_weather=true&temperature_unit=fahrenheit")
10681d05cddcSAtari911                    .then(response => response.json())
10691d05cddcSAtari911                    .then(data => {
10701d05cddcSAtari911                        if (data.current_weather) {
10711d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
10721d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
10731d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
10741d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
10751d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
10761d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
10771d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
10781d05cddcSAtari911                        }
10791d05cddcSAtari911                    })
10801d05cddcSAtari911                    .catch(err => console.log("Weather error:", err));
10811d05cddcSAtari911            });
10821d05cddcSAtari911        } else {
10831d05cddcSAtari911            // No geolocation, use Sacramento
10841d05cddcSAtari911            fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944&current_weather=true&temperature_unit=fahrenheit")
10851d05cddcSAtari911                .then(response => response.json())
10861d05cddcSAtari911                .then(data => {
10871d05cddcSAtari911                    if (data.current_weather) {
10881d05cddcSAtari911                        const temp = Math.round(data.current_weather.temperature);
10891d05cddcSAtari911                        const weatherCode = data.current_weather.weathercode;
10901d05cddcSAtari911                        const icon = getWeatherIcon(weatherCode);
10911d05cddcSAtari911                        const iconEl = document.getElementById("weather-icon-' . $calId . '");
10921d05cddcSAtari911                        const tempEl = document.getElementById("weather-temp-' . $calId . '");
10931d05cddcSAtari911                        if (iconEl) iconEl.textContent = icon;
10941d05cddcSAtari911                        if (tempEl) tempEl.innerHTML = temp + "&deg;";
10951d05cddcSAtari911                    }
10961d05cddcSAtari911                })
10971d05cddcSAtari911                .catch(err => console.log("Weather error:", err));
10981d05cddcSAtari911        }
10991d05cddcSAtari911    }
11001d05cddcSAtari911
11011d05cddcSAtari911    // WMO Weather interpretation codes
11021d05cddcSAtari911    function getWeatherIcon(code) {
11031d05cddcSAtari911        const icons = {
11041d05cddcSAtari911            0: "☀️",   // Clear sky
11051d05cddcSAtari911            1: "��️",   // Mainly clear
11061d05cddcSAtari911            2: "⛅",   // Partly cloudy
11071d05cddcSAtari911            3: "☁️",   // Overcast
11081d05cddcSAtari911            45: "��️",  // Fog
11091d05cddcSAtari911            48: "��️",  // Depositing rime fog
11101d05cddcSAtari911            51: "��️",  // Light drizzle
11111d05cddcSAtari911            53: "��️",  // Moderate drizzle
11121d05cddcSAtari911            55: "��️",  // Dense drizzle
11131d05cddcSAtari911            61: "��️",  // Slight rain
11141d05cddcSAtari911            63: "��️",  // Moderate rain
11151d05cddcSAtari911            65: "⛈️",  // Heavy rain
11161d05cddcSAtari911            71: "��️",  // Slight snow
11171d05cddcSAtari911            73: "��️",  // Moderate snow
11181d05cddcSAtari911            75: "❄️",  // Heavy snow
11191d05cddcSAtari911            77: "��️",  // Snow grains
11201d05cddcSAtari911            80: "��️",  // Slight rain showers
11211d05cddcSAtari911            81: "��️",  // Moderate rain showers
11221d05cddcSAtari911            82: "⛈️",  // Violent rain showers
11231d05cddcSAtari911            85: "��️",  // Slight snow showers
11241d05cddcSAtari911            86: "❄️",  // Heavy snow showers
11251d05cddcSAtari911            95: "⛈️",  // Thunderstorm
11261d05cddcSAtari911            96: "⛈️",  // Thunderstorm with slight hail
11271d05cddcSAtari911            99: "⛈️"   // Thunderstorm with heavy hail
11281d05cddcSAtari911        };
11291d05cddcSAtari911        return icons[code] || "��️";
11301d05cddcSAtari911    }
11311d05cddcSAtari911
11321d05cddcSAtari911    // Update weather immediately and every 10 minutes
11331d05cddcSAtari911    updateWeather();
11341d05cddcSAtari911    setInterval(updateWeather, 600000);
11351d05cddcSAtari911
11361d05cddcSAtari911    // CPU load history for 4-second rolling average
11371d05cddcSAtari911    const cpuHistory = [];
11381d05cddcSAtari911    const CPU_HISTORY_SIZE = 2; // 2 samples × 2 seconds = 4 seconds
11391d05cddcSAtari911
11401d05cddcSAtari911    // Store latest system stats for tooltips
11411d05cddcSAtari911    let latestStats = {
11421d05cddcSAtari911        load: {"1min": 0, "5min": 0, "15min": 0},
11431d05cddcSAtari911        uptime: "",
11441d05cddcSAtari911        memory_details: {},
11451d05cddcSAtari911        top_processes: []
11461d05cddcSAtari911    };
11471d05cddcSAtari911
11481d05cddcSAtari911    // Tooltip functions
11491d05cddcSAtari911    window["showTooltip_' . $calId . '"] = function(color) {
11501d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
11511d05cddcSAtari911        if (!tooltip) {
11521d05cddcSAtari911            console.log("Tooltip element not found for color:", color);
11531d05cddcSAtari911            return;
11541d05cddcSAtari911        }
11551d05cddcSAtari911
11561d05cddcSAtari911        console.log("Showing tooltip for:", color, "latestStats:", latestStats);
11571d05cddcSAtari911
11581d05cddcSAtari911        let content = "";
11591d05cddcSAtari911
11601d05cddcSAtari911        if (color === "green") {
11611d05cddcSAtari911            // Green bar: Load averages and uptime
11621d05cddcSAtari911            content = "<div class=\"tooltip-title\">CPU Load Average</div>";
11631d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
11641d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
11651d05cddcSAtari911            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
11661d05cddcSAtari911            if (latestStats.uptime) {
11671d05cddcSAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\">Uptime: " + latestStats.uptime + "</div>";
11681d05cddcSAtari911            }
11691d05cddcSAtari911            tooltip.style.borderColor = "#00cc07";
11701d05cddcSAtari911            tooltip.style.color = "#00cc07";
11711d05cddcSAtari911        } else if (color === "purple") {
11721d05cddcSAtari911            // Purple bar: Load averages (short-term) and top processes
11731d05cddcSAtari911            content = "<div class=\"tooltip-title\">CPU Load (Short-term)</div>";
11741d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
11751d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
11761d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
11771d05cddcSAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(155,89,182,0.3);\" class=\"tooltip-title\">Top Processes</div>";
11781d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
11791d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
11801d05cddcSAtari911                });
11811d05cddcSAtari911            }
11821d05cddcSAtari911            tooltip.style.borderColor = "#9b59b6";
11831d05cddcSAtari911            tooltip.style.color = "#9b59b6";
11841d05cddcSAtari911        } else if (color === "orange") {
11851d05cddcSAtari911            // Orange bar: Memory details and top processes
11861d05cddcSAtari911            content = "<div class=\"tooltip-title\">Memory Usage</div>";
11871d05cddcSAtari911            if (latestStats.memory_details && latestStats.memory_details.total) {
11881d05cddcSAtari911                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
11891d05cddcSAtari911                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
11901d05cddcSAtari911                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
11911d05cddcSAtari911                if (latestStats.memory_details.cached) {
11921d05cddcSAtari911                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
11931d05cddcSAtari911                }
11941d05cddcSAtari911            } else {
11951d05cddcSAtari911                content += "<div>Loading...</div>";
11961d05cddcSAtari911            }
11971d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
11981d05cddcSAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(255,140,0,0.3);\" class=\"tooltip-title\">Top Processes</div>";
11991d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
12001d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
12011d05cddcSAtari911                });
12021d05cddcSAtari911            }
12031d05cddcSAtari911            tooltip.style.borderColor = "#ff9800";
12041d05cddcSAtari911            tooltip.style.color = "#ff9800";
12051d05cddcSAtari911        }
12061d05cddcSAtari911
12071d05cddcSAtari911        console.log("Tooltip content:", content);
12081d05cddcSAtari911        tooltip.innerHTML = content;
12091d05cddcSAtari911        tooltip.style.display = "block";
12101d05cddcSAtari911
12111d05cddcSAtari911        // Position tooltip using fixed positioning above the bar
12121d05cddcSAtari911        const bar = tooltip.parentElement;
12131d05cddcSAtari911        const barRect = bar.getBoundingClientRect();
12141d05cddcSAtari911        const tooltipRect = tooltip.getBoundingClientRect();
12151d05cddcSAtari911
12161d05cddcSAtari911        // Center horizontally on the bar
12171d05cddcSAtari911        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
12181d05cddcSAtari911        // Position above the bar with 8px gap
12191d05cddcSAtari911        const top = barRect.top - tooltipRect.height - 8;
12201d05cddcSAtari911
12211d05cddcSAtari911        tooltip.style.left = left + "px";
12221d05cddcSAtari911        tooltip.style.top = top + "px";
12231d05cddcSAtari911    };
12241d05cddcSAtari911
12251d05cddcSAtari911    window["hideTooltip_' . $calId . '"] = function(color) {
12261d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
12271d05cddcSAtari911        if (tooltip) {
12281d05cddcSAtari911            tooltip.style.display = "none";
12291d05cddcSAtari911        }
12301d05cddcSAtari911    };
12311d05cddcSAtari911
12321d05cddcSAtari911    // Update CPU and memory bars every 2 seconds
12331d05cddcSAtari911    function updateSystemStats() {
12341d05cddcSAtari911        // Fetch real system stats from server
12351d05cddcSAtari911        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
12361d05cddcSAtari911            .then(response => response.json())
12371d05cddcSAtari911            .then(data => {
12381d05cddcSAtari911                console.log("System stats received:", data);
12391d05cddcSAtari911
12401d05cddcSAtari911                // Store data for tooltips
12411d05cddcSAtari911                latestStats = {
12421d05cddcSAtari911                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
12431d05cddcSAtari911                    uptime: data.uptime || "",
12441d05cddcSAtari911                    memory_details: data.memory_details || {},
12451d05cddcSAtari911                    top_processes: data.top_processes || []
12461d05cddcSAtari911                };
12471d05cddcSAtari911
12481d05cddcSAtari911                console.log("latestStats updated to:", latestStats);
12491d05cddcSAtari911
12501d05cddcSAtari911                // Update green bar (5-minute average) - updates live now!
12511d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
12521d05cddcSAtari911                if (greenBar) {
12531d05cddcSAtari911                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
12541d05cddcSAtari911                }
12551d05cddcSAtari911
12561d05cddcSAtari911                // Add current CPU to history for purple bar
12571d05cddcSAtari911                cpuHistory.push(data.cpu);
12581d05cddcSAtari911                if (cpuHistory.length > CPU_HISTORY_SIZE) {
12591d05cddcSAtari911                    cpuHistory.shift(); // Remove oldest
12601d05cddcSAtari911                }
12611d05cddcSAtari911
12621d05cddcSAtari911                // Calculate 5-second average for CPU
12631d05cddcSAtari911                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
12641d05cddcSAtari911
12651d05cddcSAtari911                // Update CPU bar (purple) with 5-second average
12661d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
12671d05cddcSAtari911                if (cpuBar) {
12681d05cddcSAtari911                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
12691d05cddcSAtari911                }
12701d05cddcSAtari911
12711d05cddcSAtari911                // Update memory bar (orange) with real data
12721d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
12731d05cddcSAtari911                if (memBar) {
12741d05cddcSAtari911                    memBar.style.width = Math.min(100, data.memory) + "%";
12751d05cddcSAtari911                }
12761d05cddcSAtari911            })
12771d05cddcSAtari911            .catch(error => {
12781d05cddcSAtari911                console.log("System stats error:", error);
12791d05cddcSAtari911                // Fallback to client-side estimates on error
12801d05cddcSAtari911                const cpuFallback = Math.random() * 100;
12811d05cddcSAtari911                cpuHistory.push(cpuFallback);
12821d05cddcSAtari911                if (cpuHistory.length > CPU_HISTORY_SIZE) {
12831d05cddcSAtari911                    cpuHistory.shift();
12841d05cddcSAtari911                }
12851d05cddcSAtari911                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
12861d05cddcSAtari911
12871d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
12881d05cddcSAtari911                if (greenBar) greenBar.style.width = Math.min(100, cpuFallback) + "%";
12891d05cddcSAtari911
12901d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
12911d05cddcSAtari911                if (cpuBar) cpuBar.style.width = Math.min(100, cpuAverage) + "%";
12921d05cddcSAtari911
12931d05cddcSAtari911                let memoryUsage = 0;
12941d05cddcSAtari911                if (performance.memory) {
12951d05cddcSAtari911                    memoryUsage = (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100;
12961d05cddcSAtari911                } else {
12971d05cddcSAtari911                    memoryUsage = Math.random() * 100;
12981d05cddcSAtari911                }
12991d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
13001d05cddcSAtari911                if (memBar) memBar.style.width = Math.min(100, memoryUsage) + "%";
13011d05cddcSAtari911            });
13021d05cddcSAtari911    }
13031d05cddcSAtari911
13041d05cddcSAtari911    // Update immediately and then every 2 seconds
13051d05cddcSAtari911    updateSystemStats();
13061d05cddcSAtari911    setInterval(updateSystemStats, 2000);
13071d05cddcSAtari911})();
13081d05cddcSAtari911</script>';
13091d05cddcSAtari911        }
131019378907SAtari911
131119378907SAtari911        if (empty($allEvents)) {
1312e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-empty">';
1313e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText);
1314e3a9f44cSAtari911            if ($namespace) {
1315e3a9f44cSAtari911                $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>';
131687ac9bf3SAtari911            }
1317e3a9f44cSAtari911            $html .= '</div>';
1318e3a9f44cSAtari911            $html .= '<div class="eventlist-simple-body">No events</div>';
1319e3a9f44cSAtari911            $html .= '</div>';
1320e3a9f44cSAtari911        } else {
1321e3a9f44cSAtari911            // Calculate today and tomorrow's dates for highlighting
13221d05cddcSAtari911            $todayStr = date('Y-m-d');
1323e3a9f44cSAtari911            $tomorrow = date('Y-m-d', strtotime('+1 day'));
1324e3a9f44cSAtari911
1325e3a9f44cSAtari911            foreach ($allEvents as $dateKey => $dayEvents) {
1326e3a9f44cSAtari911                $dateObj = new DateTime($dateKey);
1327e3a9f44cSAtari911                $displayDate = $dateObj->format('D, M j');
1328e3a9f44cSAtari911
13291d05cddcSAtari911                // Check if this date is today or tomorrow or past
1330e3a9f44cSAtari911                // Enable highlighting for sidebar mode AND range modes (day, week, month)
1331e3a9f44cSAtari911                $enableHighlighting = $sidebar || !empty($range);
13321d05cddcSAtari911                $isToday = $enableHighlighting && ($dateKey === $todayStr);
1333e3a9f44cSAtari911                $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow);
13341d05cddcSAtari911                $isPast = $dateKey < $todayStr;
133519378907SAtari911
133619378907SAtari911                foreach ($dayEvents as $event) {
13371d05cddcSAtari911                    // Check if this is a task and if it's completed
13381d05cddcSAtari911                    $isTask = !empty($event['isTask']);
13391d05cddcSAtari911                    $completed = !empty($event['completed']);
13401d05cddcSAtari911
13411d05cddcSAtari911                    // ALWAYS skip completed tasks UNLESS showchecked is explicitly set
13421d05cddcSAtari911                    if (!$showchecked && $isTask && $completed) {
1343e3a9f44cSAtari911                        continue;
1344e3a9f44cSAtari911                    }
134519378907SAtari911
13461d05cddcSAtari911                    // Skip past events that are NOT tasks (only show past due tasks from the past)
13471d05cddcSAtari911                    if ($isPast && !$isTask) {
13481d05cddcSAtari911                        continue;
13491d05cddcSAtari911                    }
13501d05cddcSAtari911
13511d05cddcSAtari911                    // Determine if task is past due (past date, is task, not completed)
13521d05cddcSAtari911                    $isPastDue = $isPast && $isTask && !$completed;
13531d05cddcSAtari911
1354e3a9f44cSAtari911                    // Line 1: Header (Title, Time, Date, Namespace)
1355e3a9f44cSAtari911                    $todayClass = $isToday ? ' eventlist-simple-today' : '';
1356e3a9f44cSAtari911                    $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : '';
13571d05cddcSAtari911                    $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : '';
13581d05cddcSAtari911                    $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . $pastDueClass . '">';
1359e3a9f44cSAtari911                    $html .= '<div class="eventlist-simple-header">';
1360e3a9f44cSAtari911
1361e3a9f44cSAtari911                    // Title
1362e3a9f44cSAtari911                    $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>';
1363e3a9f44cSAtari911
1364e3a9f44cSAtari911                    // Time (12-hour format)
1365e3a9f44cSAtari911                    if (!empty($event['time'])) {
1366e3a9f44cSAtari911                        $timeParts = explode(':', $event['time']);
136787ac9bf3SAtari911                        if (count($timeParts) === 2) {
136887ac9bf3SAtari911                            $hour = (int)$timeParts[0];
136987ac9bf3SAtari911                            $minute = $timeParts[1];
137087ac9bf3SAtari911                            $ampm = $hour >= 12 ? 'PM' : 'AM';
1371e3a9f44cSAtari911                            $hour = $hour % 12 ?: 12;
137287ac9bf3SAtari911                            $displayTime = $hour . ':' . $minute . ' ' . $ampm;
1373e3a9f44cSAtari911                            $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>';
137419378907SAtari911                        }
137587ac9bf3SAtari911                    }
137687ac9bf3SAtari911
1377e3a9f44cSAtari911                    // Date
1378e3a9f44cSAtari911                    $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>';
1379e3a9f44cSAtari911
13801d05cddcSAtari911                    // Badge: PAST DUE, TODAY, or nothing
13811d05cddcSAtari911                    if ($isPastDue) {
13821d05cddcSAtari911                        $html .= ' <span class="eventlist-simple-pastdue-badge">PAST DUE</span>';
13831d05cddcSAtari911                    } elseif ($isToday) {
1384e3a9f44cSAtari911                        $html .= ' <span class="eventlist-simple-today-badge">TODAY</span>';
138587ac9bf3SAtari911                    }
1386e3a9f44cSAtari911
1387e3a9f44cSAtari911                    // Namespace badge (show individual event's namespace)
1388e3a9f44cSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
1389e3a9f44cSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
1390e3a9f44cSAtari911                        $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading
139119378907SAtari911                    }
1392e3a9f44cSAtari911                    if ($eventNamespace) {
1393e3a9f44cSAtari911                        $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>';
1394e3a9f44cSAtari911                    }
1395e3a9f44cSAtari911
1396e3a9f44cSAtari911                    $html .= '</div>'; // header
1397e3a9f44cSAtari911
1398e3a9f44cSAtari911                    // Line 2: Body (Description only) - only show if description exists
1399e3a9f44cSAtari911                    if (!empty($event['description'])) {
1400e3a9f44cSAtari911                        $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>';
1401e3a9f44cSAtari911                    }
1402e3a9f44cSAtari911
1403e3a9f44cSAtari911                    $html .= '</div>'; // item
140419378907SAtari911                }
140519378907SAtari911            }
140687ac9bf3SAtari911        }
140719378907SAtari911
1408e3a9f44cSAtari911        $html .= '</div>'; // eventlist-simple
140919378907SAtari911
141019378907SAtari911        return $html;
141119378907SAtari911    }
141219378907SAtari911
141319378907SAtari911    private function renderEventDialog($calId, $namespace) {
141419378907SAtari911        $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">';
141519378907SAtari911        $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>';
141619378907SAtari911
141719378907SAtari911        // Draggable dialog
141819378907SAtari911        $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">';
141919378907SAtari911
142019378907SAtari911        // Header with drag handle and close button
142119378907SAtari911        $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">';
142219378907SAtari911        $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>';
142319378907SAtari911        $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>';
142419378907SAtari911        $html .= '</div>';
142519378907SAtari911
142619378907SAtari911        // Form content
142719378907SAtari911        $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">';
142819378907SAtari911
142919378907SAtari911        // Hidden ID field
143019378907SAtari911        $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">';
143119378907SAtari911
14321d05cddcSAtari911        // 1. TITLE
14331d05cddcSAtari911        $html .= '<div class="form-field">';
14341d05cddcSAtari911        $html .= '<label class="field-label">�� Title</label>';
14351d05cddcSAtari911        $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek input-compact" placeholder="Event or task title...">';
143619378907SAtari911        $html .= '</div>';
143719378907SAtari911
14381d05cddcSAtari911        // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching)
14391d05cddcSAtari911        $html .= '<div class="form-field">';
14401d05cddcSAtari911        $html .= '<label class="field-label">�� Namespace</label>';
14411d05cddcSAtari911
14421d05cddcSAtari911        // Hidden field to store actual selected namespace
14431d05cddcSAtari911        $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">';
14441d05cddcSAtari911
14451d05cddcSAtari911        // Searchable input
14461d05cddcSAtari911        $html .= '<div class="namespace-search-wrapper">';
14471d05cddcSAtari911        $html .= '<input type="text" id="event-namespace-search-' . $calId . '" class="input-sleek input-compact namespace-search-input" placeholder="Type to search or leave empty for default..." autocomplete="off">';
14481d05cddcSAtari911        $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>';
14491d05cddcSAtari911        $html .= '</div>';
14501d05cddcSAtari911
14511d05cddcSAtari911        // Store namespaces as JSON for JavaScript
14521d05cddcSAtari911        $allNamespaces = $this->getAllNamespaces();
14531d05cddcSAtari911        $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>';
14541d05cddcSAtari911
14551d05cddcSAtari911        $html .= '</div>';
14561d05cddcSAtari911
14571d05cddcSAtari911        // 2. DESCRIPTION
14581d05cddcSAtari911        $html .= '<div class="form-field">';
14591d05cddcSAtari911        $html .= '<label class="field-label">�� Description</label>';
14601d05cddcSAtari911        $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="1" class="input-sleek textarea-sleek textarea-compact" placeholder="Optional details..."></textarea>';
14611d05cddcSAtari911        $html .= '</div>';
14621d05cddcSAtari911
14631d05cddcSAtari911        // 3. START DATE - END DATE (inline)
146419378907SAtari911        $html .= '<div class="form-row-group">';
146519378907SAtari911
14661d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
14671d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Start Date</label>';
14681d05cddcSAtari911        $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date input-compact">';
146919378907SAtari911        $html .= '</div>';
147019378907SAtari911
14711d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
14721d05cddcSAtari911        $html .= '<label class="field-label-compact">�� End Date</label>';
14731d05cddcSAtari911        $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date input-compact" placeholder="Optional">';
147419378907SAtari911        $html .= '</div>';
147519378907SAtari911
14761d05cddcSAtari911        $html .= '</div>'; // End row
147719378907SAtari911
14781d05cddcSAtari911        // 4. IS REPEATING CHECKBOX
14791d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
14801d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
148187ac9bf3SAtari911        $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">';
148287ac9bf3SAtari911        $html .= '<span>�� Repeating Event</span>';
148387ac9bf3SAtari911        $html .= '</label>';
148487ac9bf3SAtari911        $html .= '</div>';
148587ac9bf3SAtari911
14861d05cddcSAtari911        // Recurring options (shown when checkbox is checked)
148787ac9bf3SAtari911        $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none;">';
148887ac9bf3SAtari911
14891d05cddcSAtari911        $html .= '<div class="form-row-group">';
14901d05cddcSAtari911
14911d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
14921d05cddcSAtari911        $html .= '<label class="field-label-compact">Repeat Every</label>';
14931d05cddcSAtari911        $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek input-compact">';
149487ac9bf3SAtari911        $html .= '<option value="daily">Daily</option>';
149587ac9bf3SAtari911        $html .= '<option value="weekly">Weekly</option>';
149687ac9bf3SAtari911        $html .= '<option value="monthly">Monthly</option>';
149787ac9bf3SAtari911        $html .= '<option value="yearly">Yearly</option>';
149887ac9bf3SAtari911        $html .= '</select>';
149987ac9bf3SAtari911        $html .= '</div>';
150087ac9bf3SAtari911
15011d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
15021d05cddcSAtari911        $html .= '<label class="field-label-compact">Repeat Until</label>';
15031d05cddcSAtari911        $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">';
150487ac9bf3SAtari911        $html .= '</div>';
150587ac9bf3SAtari911
15061d05cddcSAtari911        $html .= '</div>'; // End row
15071d05cddcSAtari911        $html .= '</div>'; // End recurring options
150887ac9bf3SAtari911
15091d05cddcSAtari911        // 5. TIME (Start & End) - COLOR (inline)
15101d05cddcSAtari911        $html .= '<div class="form-row-group">';
15111d05cddcSAtari911
15121d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
15131d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Start Time</label>';
15141d05cddcSAtari911        $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek input-compact" onchange="updateEndTimeOptions(\'' . $calId . '\')">';
15151d05cddcSAtari911        $html .= '<option value="">All day</option>';
1516e3a9f44cSAtari911
1517e3a9f44cSAtari911        // Generate time options in 15-minute intervals
1518e3a9f44cSAtari911        for ($hour = 0; $hour < 24; $hour++) {
1519e3a9f44cSAtari911            for ($minute = 0; $minute < 60; $minute += 15) {
1520e3a9f44cSAtari911                $timeValue = sprintf('%02d:%02d', $hour, $minute);
1521e3a9f44cSAtari911                $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
1522e3a9f44cSAtari911                $ampm = $hour < 12 ? 'AM' : 'PM';
1523e3a9f44cSAtari911                $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
1524e3a9f44cSAtari911                $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
1525e3a9f44cSAtari911            }
1526e3a9f44cSAtari911        }
1527e3a9f44cSAtari911
1528e3a9f44cSAtari911        $html .= '</select>';
152919378907SAtari911        $html .= '</div>';
153019378907SAtari911
15311d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
15321d05cddcSAtari911        $html .= '<label class="field-label-compact">�� End Time</label>';
15331d05cddcSAtari911        $html .= '<select id="event-end-time-' . $calId . '" name="endTime" class="input-sleek input-compact">';
15341d05cddcSAtari911        $html .= '<option value="">Same as start</option>';
15351d05cddcSAtari911
15361d05cddcSAtari911        // Generate time options in 15-minute intervals
15371d05cddcSAtari911        for ($hour = 0; $hour < 24; $hour++) {
15381d05cddcSAtari911            for ($minute = 0; $minute < 60; $minute += 15) {
15391d05cddcSAtari911                $timeValue = sprintf('%02d:%02d', $hour, $minute);
15401d05cddcSAtari911                $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
15411d05cddcSAtari911                $ampm = $hour < 12 ? 'AM' : 'PM';
15421d05cddcSAtari911                $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
15431d05cddcSAtari911                $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
15441d05cddcSAtari911            }
15451d05cddcSAtari911        }
15461d05cddcSAtari911
15471d05cddcSAtari911        $html .= '</select>';
154819378907SAtari911        $html .= '</div>';
154919378907SAtari911
15501d05cddcSAtari911        $html .= '</div>'; // End row
15511d05cddcSAtari911
15521d05cddcSAtari911        // Color field (new row)
15531d05cddcSAtari911        $html .= '<div class="form-row-group">';
15541d05cddcSAtari911
15551d05cddcSAtari911        $html .= '<div class="form-field form-field-full">';
15561d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Color</label>';
15571d05cddcSAtari911        $html .= '<div class="color-picker-wrapper">';
15581d05cddcSAtari911        $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">';
15591d05cddcSAtari911        $html .= '<option value="#3498db" style="background:#3498db;color:white">�� Blue</option>';
15601d05cddcSAtari911        $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white">�� Green</option>';
15611d05cddcSAtari911        $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white">�� Red</option>';
15621d05cddcSAtari911        $html .= '<option value="#f39c12" style="background:#f39c12;color:white">�� Orange</option>';
15631d05cddcSAtari911        $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white">�� Purple</option>';
15641d05cddcSAtari911        $html .= '<option value="#e91e63" style="background:#e91e63;color:white">�� Pink</option>';
15651d05cddcSAtari911        $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white">�� Teal</option>';
15661d05cddcSAtari911        $html .= '<option value="custom">�� Custom...</option>';
15671d05cddcSAtari911        $html .= '</select>';
15681d05cddcSAtari911        $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">';
15691d05cddcSAtari911        $html .= '</div>';
157019378907SAtari911        $html .= '</div>';
157119378907SAtari911
15721d05cddcSAtari911        $html .= '</div>'; // End row
15731d05cddcSAtari911
15741d05cddcSAtari911        // Task checkbox
15751d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
15761d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
15771d05cddcSAtari911        $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">';
15781d05cddcSAtari911        $html .= '<span>�� This is a task (can be checked off)</span>';
15791d05cddcSAtari911        $html .= '</label>';
158019378907SAtari911        $html .= '</div>';
158119378907SAtari911
158219378907SAtari911        // Action buttons
158319378907SAtari911        $html .= '<div class="dialog-actions-sleek">';
158419378907SAtari911        $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>';
158519378907SAtari911        $html .= '<button type="submit" class="btn-sleek btn-save-sleek">�� Save</button>';
158619378907SAtari911        $html .= '</div>';
158719378907SAtari911
158819378907SAtari911        $html .= '</form>';
158919378907SAtari911        $html .= '</div>';
159019378907SAtari911        $html .= '</div>';
159119378907SAtari911
159219378907SAtari911        return $html;
159319378907SAtari911    }
159419378907SAtari911
159587ac9bf3SAtari911    private function renderMonthPicker($calId, $year, $month, $namespace) {
159687ac9bf3SAtari911        $html = '<div class="month-picker-overlay" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">';
159787ac9bf3SAtari911        $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">';
159887ac9bf3SAtari911        $html .= '<h4>Jump to Month</h4>';
159987ac9bf3SAtari911
160087ac9bf3SAtari911        $html .= '<div class="month-picker-selects">';
160187ac9bf3SAtari911        $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">';
160287ac9bf3SAtari911        $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
160387ac9bf3SAtari911        for ($m = 1; $m <= 12; $m++) {
160487ac9bf3SAtari911            $selected = ($m == $month) ? ' selected' : '';
160587ac9bf3SAtari911            $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>';
160687ac9bf3SAtari911        }
160787ac9bf3SAtari911        $html .= '</select>';
160887ac9bf3SAtari911
160987ac9bf3SAtari911        $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">';
161087ac9bf3SAtari911        $currentYear = (int)date('Y');
161187ac9bf3SAtari911        for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) {
161287ac9bf3SAtari911            $selected = ($y == $year) ? ' selected' : '';
161387ac9bf3SAtari911            $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>';
161487ac9bf3SAtari911        }
161587ac9bf3SAtari911        $html .= '</select>';
161687ac9bf3SAtari911        $html .= '</div>';
161787ac9bf3SAtari911
161887ac9bf3SAtari911        $html .= '<div class="month-picker-actions">';
161987ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>';
162087ac9bf3SAtari911        $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>';
162187ac9bf3SAtari911        $html .= '</div>';
162287ac9bf3SAtari911
162387ac9bf3SAtari911        $html .= '</div>';
162487ac9bf3SAtari911        $html .= '</div>';
162587ac9bf3SAtari911
162687ac9bf3SAtari911        return $html;
162787ac9bf3SAtari911    }
162887ac9bf3SAtari911
162919378907SAtari911    private function renderDescription($description) {
163019378907SAtari911        if (empty($description)) {
163119378907SAtari911            return '';
163219378907SAtari911        }
163319378907SAtari911
1634e3a9f44cSAtari911        // Token-based parsing to avoid escaping issues
1635e3a9f44cSAtari911        $rendered = $description;
1636e3a9f44cSAtari911        $tokens = array();
1637e3a9f44cSAtari911        $tokenIndex = 0;
163819378907SAtari911
1639e3a9f44cSAtari911        // Convert DokuWiki image syntax {{image.jpg}} to tokens
1640e3a9f44cSAtari911        $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/';
1641e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1642e3a9f44cSAtari911        foreach ($matches as $match) {
1643e3a9f44cSAtari911            $imagePath = trim($match[1]);
1644e3a9f44cSAtari911            $alt = isset($match[2]) ? trim($match[2]) : '';
164519378907SAtari911
1646e3a9f44cSAtari911            // Handle external URLs
164719378907SAtari911            if (preg_match('/^https?:\/\//', $imagePath)) {
1648e3a9f44cSAtari911                $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
1649e3a9f44cSAtari911            } else {
165019378907SAtari911                // Handle internal DokuWiki images
165119378907SAtari911                $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath);
1652e3a9f44cSAtari911                $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
1653e3a9f44cSAtari911            }
165419378907SAtari911
1655e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
1656e3a9f44cSAtari911            $tokens[$tokenIndex] = $imageHtml;
1657e3a9f44cSAtari911            $tokenIndex++;
1658e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
1659e3a9f44cSAtari911        }
1660e3a9f44cSAtari911
1661e3a9f44cSAtari911        // Convert DokuWiki link syntax [[link|text]] to tokens
1662e3a9f44cSAtari911        $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/';
1663e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1664e3a9f44cSAtari911        foreach ($matches as $match) {
1665e3a9f44cSAtari911            $link = trim($match[1]);
1666e3a9f44cSAtari911            $text = isset($match[2]) ? trim($match[2]) : $link;
166719378907SAtari911
166819378907SAtari911            // Handle external URLs
166919378907SAtari911            if (preg_match('/^https?:\/\//', $link)) {
1670e3a9f44cSAtari911                $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>';
1671e3a9f44cSAtari911            } else {
167287ac9bf3SAtari911                // Handle internal DokuWiki links with section anchors
167387ac9bf3SAtari911                $parts = explode('#', $link, 2);
167487ac9bf3SAtari911                $pagePart = $parts[0];
167587ac9bf3SAtari911                $sectionPart = isset($parts[1]) ? '#' . $parts[1] : '';
167687ac9bf3SAtari911
167787ac9bf3SAtari911                $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart;
1678e3a9f44cSAtari911                $linkHtml = '<a href="' . $wikiUrl . '">' . htmlspecialchars($text) . '</a>';
167919378907SAtari911            }
168019378907SAtari911
1681e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
1682e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
1683e3a9f44cSAtari911            $tokenIndex++;
1684e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
1685e3a9f44cSAtari911        }
168619378907SAtari911
1687e3a9f44cSAtari911        // Convert markdown-style links [text](url) to tokens
1688e3a9f44cSAtari911        $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
1689e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1690e3a9f44cSAtari911        foreach ($matches as $match) {
1691e3a9f44cSAtari911            $text = trim($match[1]);
1692e3a9f44cSAtari911            $url = trim($match[2]);
169319378907SAtari911
1694e3a9f44cSAtari911            if (preg_match('/^https?:\/\//', $url)) {
1695e3a9f44cSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>';
1696e3a9f44cSAtari911            } else {
1697e3a9f44cSAtari911                $linkHtml = '<a href="' . htmlspecialchars($url) . '">' . htmlspecialchars($text) . '</a>';
1698e3a9f44cSAtari911            }
1699e3a9f44cSAtari911
1700e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
1701e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
1702e3a9f44cSAtari911            $tokenIndex++;
1703e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
1704e3a9f44cSAtari911        }
1705e3a9f44cSAtari911
1706e3a9f44cSAtari911        // Convert plain URLs to tokens
1707e3a9f44cSAtari911        $pattern = '/(https?:\/\/[^\s<]+)/';
1708e3a9f44cSAtari911        preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
1709e3a9f44cSAtari911        foreach ($matches as $match) {
1710e3a9f44cSAtari911            $url = $match[1];
1711e3a9f44cSAtari911            $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($url) . '</a>';
1712e3a9f44cSAtari911
1713e3a9f44cSAtari911            $token = "\x00TOKEN" . $tokenIndex . "\x00";
1714e3a9f44cSAtari911            $tokens[$tokenIndex] = $linkHtml;
1715e3a9f44cSAtari911            $tokenIndex++;
1716e3a9f44cSAtari911            $rendered = str_replace($match[0], $token, $rendered);
1717e3a9f44cSAtari911        }
1718e3a9f44cSAtari911
1719e3a9f44cSAtari911        // NOW escape HTML (tokens are protected)
1720e3a9f44cSAtari911        $rendered = htmlspecialchars($rendered);
1721e3a9f44cSAtari911
1722e3a9f44cSAtari911        // Convert newlines to <br>
1723e3a9f44cSAtari911        $rendered = nl2br($rendered);
1724e3a9f44cSAtari911
1725e3a9f44cSAtari911        // DokuWiki text formatting
1726e3a9f44cSAtari911        // Bold: **text** or __text__
1727e3a9f44cSAtari911        $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered);
1728e3a9f44cSAtari911        $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered);
1729e3a9f44cSAtari911
1730e3a9f44cSAtari911        // Italic: //text//
1731e3a9f44cSAtari911        $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered);
1732e3a9f44cSAtari911
1733e3a9f44cSAtari911        // Strikethrough: <del>text</del>
1734e3a9f44cSAtari911        $rendered = preg_replace('/&lt;del&gt;(.+?)&lt;\/del&gt;/', '<del>$1</del>', $rendered);
1735e3a9f44cSAtari911
1736e3a9f44cSAtari911        // Monospace: ''text''
1737e3a9f44cSAtari911        $rendered = preg_replace('/&#039;&#039;(.+?)&#039;&#039;/', '<code>$1</code>', $rendered);
1738e3a9f44cSAtari911
1739e3a9f44cSAtari911        // Subscript: <sub>text</sub>
1740e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sub&gt;(.+?)&lt;\/sub&gt;/', '<sub>$1</sub>', $rendered);
1741e3a9f44cSAtari911
1742e3a9f44cSAtari911        // Superscript: <sup>text</sup>
1743e3a9f44cSAtari911        $rendered = preg_replace('/&lt;sup&gt;(.+?)&lt;\/sup&gt;/', '<sup>$1</sup>', $rendered);
1744e3a9f44cSAtari911
1745e3a9f44cSAtari911        // Restore tokens
1746e3a9f44cSAtari911        foreach ($tokens as $i => $html) {
1747e3a9f44cSAtari911            $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered);
1748e3a9f44cSAtari911        }
174919378907SAtari911
175019378907SAtari911        return $rendered;
175119378907SAtari911    }
175219378907SAtari911
175319378907SAtari911    private function loadEvents($namespace, $year, $month) {
175419378907SAtari911        $dataDir = DOKU_INC . 'data/meta/';
175519378907SAtari911        if ($namespace) {
175619378907SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
175719378907SAtari911        }
175819378907SAtari911        $dataDir .= 'calendar/';
175919378907SAtari911
176019378907SAtari911        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
176119378907SAtari911
176219378907SAtari911        if (file_exists($eventFile)) {
176319378907SAtari911            $json = file_get_contents($eventFile);
176419378907SAtari911            return json_decode($json, true);
176519378907SAtari911        }
176619378907SAtari911
176719378907SAtari911        return array();
176819378907SAtari911    }
1769e3a9f44cSAtari911
1770e3a9f44cSAtari911    private function loadEventsMultiNamespace($namespaces, $year, $month) {
1771e3a9f44cSAtari911        // Check for wildcard pattern (namespace:*)
1772e3a9f44cSAtari911        if (preg_match('/^(.+):\*$/', $namespaces, $matches)) {
1773e3a9f44cSAtari911            $baseNamespace = $matches[1];
1774e3a9f44cSAtari911            return $this->loadEventsWildcard($baseNamespace, $year, $month);
1775e3a9f44cSAtari911        }
1776e3a9f44cSAtari911
1777e3a9f44cSAtari911        // Check for root wildcard (just *)
1778e3a9f44cSAtari911        if ($namespaces === '*') {
1779e3a9f44cSAtari911            return $this->loadEventsWildcard('', $year, $month);
1780e3a9f44cSAtari911        }
1781e3a9f44cSAtari911
1782e3a9f44cSAtari911        // Parse namespace list (semicolon separated)
1783e3a9f44cSAtari911        // e.g., "team:projects;personal;work:tasks" = three namespaces
1784e3a9f44cSAtari911        $namespaceList = array_map('trim', explode(';', $namespaces));
1785e3a9f44cSAtari911
1786e3a9f44cSAtari911        // Load events from all namespaces
1787e3a9f44cSAtari911        $allEvents = array();
1788e3a9f44cSAtari911        foreach ($namespaceList as $ns) {
1789e3a9f44cSAtari911            $ns = trim($ns);
1790e3a9f44cSAtari911            if (empty($ns)) continue;
1791e3a9f44cSAtari911
1792e3a9f44cSAtari911            $events = $this->loadEvents($ns, $year, $month);
1793e3a9f44cSAtari911
1794e3a9f44cSAtari911            // Add namespace tag to each event
1795e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
1796e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
1797e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
1798e3a9f44cSAtari911                }
1799e3a9f44cSAtari911                foreach ($dayEvents as $event) {
1800e3a9f44cSAtari911                    $event['_namespace'] = $ns;
1801e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
1802e3a9f44cSAtari911                }
1803e3a9f44cSAtari911            }
1804e3a9f44cSAtari911        }
1805e3a9f44cSAtari911
1806e3a9f44cSAtari911        return $allEvents;
1807e3a9f44cSAtari911    }
1808e3a9f44cSAtari911
1809e3a9f44cSAtari911    private function loadEventsWildcard($baseNamespace, $year, $month) {
1810e3a9f44cSAtari911        // Find all subdirectories under the base namespace
1811e3a9f44cSAtari911        $dataDir = DOKU_INC . 'data/meta/';
1812e3a9f44cSAtari911        if ($baseNamespace) {
1813e3a9f44cSAtari911            $dataDir .= str_replace(':', '/', $baseNamespace) . '/';
1814e3a9f44cSAtari911        }
1815e3a9f44cSAtari911
1816e3a9f44cSAtari911        $allEvents = array();
1817e3a9f44cSAtari911
1818e3a9f44cSAtari911        // First, load events from the base namespace itself
1819e3a9f44cSAtari911        if (empty($baseNamespace)) {
1820e3a9f44cSAtari911            // Root wildcard - load from root calendar
1821e3a9f44cSAtari911            $events = $this->loadEvents('', $year, $month);
1822e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
1823e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
1824e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
1825e3a9f44cSAtari911                }
1826e3a9f44cSAtari911                foreach ($dayEvents as $event) {
1827e3a9f44cSAtari911                    $event['_namespace'] = '';
1828e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
1829e3a9f44cSAtari911                }
1830e3a9f44cSAtari911            }
1831e3a9f44cSAtari911        } else {
1832e3a9f44cSAtari911            $events = $this->loadEvents($baseNamespace, $year, $month);
1833e3a9f44cSAtari911            foreach ($events as $dateKey => $dayEvents) {
1834e3a9f44cSAtari911                if (!isset($allEvents[$dateKey])) {
1835e3a9f44cSAtari911                    $allEvents[$dateKey] = array();
1836e3a9f44cSAtari911                }
1837e3a9f44cSAtari911                foreach ($dayEvents as $event) {
1838e3a9f44cSAtari911                    $event['_namespace'] = $baseNamespace;
1839e3a9f44cSAtari911                    $allEvents[$dateKey][] = $event;
1840e3a9f44cSAtari911                }
1841e3a9f44cSAtari911            }
1842e3a9f44cSAtari911        }
1843e3a9f44cSAtari911
1844e3a9f44cSAtari911        // Recursively find all subdirectories
1845e3a9f44cSAtari911        $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents);
1846e3a9f44cSAtari911
1847e3a9f44cSAtari911        return $allEvents;
1848e3a9f44cSAtari911    }
1849e3a9f44cSAtari911
1850e3a9f44cSAtari911    private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) {
1851e3a9f44cSAtari911        if (!is_dir($dir)) return;
1852e3a9f44cSAtari911
1853e3a9f44cSAtari911        $items = scandir($dir);
1854e3a9f44cSAtari911        foreach ($items as $item) {
1855e3a9f44cSAtari911            if ($item === '.' || $item === '..') continue;
1856e3a9f44cSAtari911
1857e3a9f44cSAtari911            $path = $dir . $item;
1858e3a9f44cSAtari911            if (is_dir($path) && $item !== 'calendar') {
1859e3a9f44cSAtari911                // This is a namespace directory
1860e3a9f44cSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
1861e3a9f44cSAtari911
1862e3a9f44cSAtari911                // Load events from this namespace
1863e3a9f44cSAtari911                $events = $this->loadEvents($namespace, $year, $month);
1864e3a9f44cSAtari911                foreach ($events as $dateKey => $dayEvents) {
1865e3a9f44cSAtari911                    if (!isset($allEvents[$dateKey])) {
1866e3a9f44cSAtari911                        $allEvents[$dateKey] = array();
1867e3a9f44cSAtari911                    }
1868e3a9f44cSAtari911                    foreach ($dayEvents as $event) {
1869e3a9f44cSAtari911                        $event['_namespace'] = $namespace;
1870e3a9f44cSAtari911                        $allEvents[$dateKey][] = $event;
1871e3a9f44cSAtari911                    }
1872e3a9f44cSAtari911                }
1873e3a9f44cSAtari911
1874e3a9f44cSAtari911                // Recurse into subdirectories
1875e3a9f44cSAtari911                $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents);
1876e3a9f44cSAtari911            }
1877e3a9f44cSAtari911        }
1878e3a9f44cSAtari911    }
18791d05cddcSAtari911
18801d05cddcSAtari911    private function getAllNamespaces() {
18811d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
18821d05cddcSAtari911        $namespaces = [];
18831d05cddcSAtari911
18841d05cddcSAtari911        // Scan for namespaces that have calendar data
18851d05cddcSAtari911        $this->scanForCalendarNamespaces($dataDir, '', $namespaces);
18861d05cddcSAtari911
18871d05cddcSAtari911        // Sort alphabetically
18881d05cddcSAtari911        sort($namespaces);
18891d05cddcSAtari911
18901d05cddcSAtari911        return $namespaces;
18911d05cddcSAtari911    }
18921d05cddcSAtari911
18931d05cddcSAtari911    private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) {
18941d05cddcSAtari911        if (!is_dir($dir)) return;
18951d05cddcSAtari911
18961d05cddcSAtari911        $items = scandir($dir);
18971d05cddcSAtari911        foreach ($items as $item) {
18981d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
18991d05cddcSAtari911
19001d05cddcSAtari911            $path = $dir . $item;
19011d05cddcSAtari911            if (is_dir($path)) {
19021d05cddcSAtari911                // Check if this directory has a calendar subdirectory with data
19031d05cddcSAtari911                $calendarDir = $path . '/calendar/';
19041d05cddcSAtari911                if (is_dir($calendarDir)) {
19051d05cddcSAtari911                    // Check if there are any JSON files in the calendar directory
19061d05cddcSAtari911                    $jsonFiles = glob($calendarDir . '*.json');
19071d05cddcSAtari911                    if (!empty($jsonFiles)) {
19081d05cddcSAtari911                        // This namespace has calendar data
19091d05cddcSAtari911                        $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
19101d05cddcSAtari911                        $namespaces[] = $namespace;
19111d05cddcSAtari911                    }
19121d05cddcSAtari911                }
19131d05cddcSAtari911
19141d05cddcSAtari911                // Recurse into subdirectories
19151d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
19161d05cddcSAtari911                $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces);
19171d05cddcSAtari911            }
19181d05cddcSAtari911        }
19191d05cddcSAtari911    }
19201d05cddcSAtari911
19211d05cddcSAtari911    /**
19221d05cddcSAtari911     * Render new sidebar widget - Week at a glance itinerary (200px wide)
19231d05cddcSAtari911     */
19241d05cddcSAtari911    private function renderSidebarWidget($events, $namespace, $calId) {
19251d05cddcSAtari911        if (empty($events)) {
19261d05cddcSAtari911            return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No events this week</div>';
19271d05cddcSAtari911        }
19281d05cddcSAtari911
19291d05cddcSAtari911        // Get important namespaces from config
19301d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
19311d05cddcSAtari911        $importantNsList = ['important']; // default
19321d05cddcSAtari911        if (file_exists($configFile)) {
19331d05cddcSAtari911            $config = include $configFile;
19341d05cddcSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
19351d05cddcSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
19361d05cddcSAtari911            }
19371d05cddcSAtari911        }
19381d05cddcSAtari911
19391d05cddcSAtari911        // Calculate date ranges
19401d05cddcSAtari911        $todayStr = date('Y-m-d');
19411d05cddcSAtari911        $tomorrowStr = date('Y-m-d', strtotime('+1 day'));
19421d05cddcSAtari911        $weekStart = date('Y-m-d', strtotime('monday this week'));
19431d05cddcSAtari911        $weekEnd = date('Y-m-d', strtotime('sunday this week'));
19441d05cddcSAtari911
19451d05cddcSAtari911        // Group events by category
19461d05cddcSAtari911        $todayEvents = [];
19471d05cddcSAtari911        $tomorrowEvents = [];
19481d05cddcSAtari911        $importantEvents = [];
19491d05cddcSAtari911        $weekEvents = []; // For week grid
19501d05cddcSAtari911
19511d05cddcSAtari911        // Process all events
19521d05cddcSAtari911        foreach ($events as $dateKey => $dayEvents) {
19531d05cddcSAtari911            // Skip events before this week
19541d05cddcSAtari911            if ($dateKey < $weekStart) continue;
19551d05cddcSAtari911
19561d05cddcSAtari911            // Initialize week grid day if in current week
19571d05cddcSAtari911            if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
19581d05cddcSAtari911                if (!isset($weekEvents[$dateKey])) {
19591d05cddcSAtari911                    $weekEvents[$dateKey] = [];
19601d05cddcSAtari911                }
19611d05cddcSAtari911            }
19621d05cddcSAtari911
19631d05cddcSAtari911            foreach ($dayEvents as $event) {
19641d05cddcSAtari911                // Add to week grid if in week range
19651d05cddcSAtari911                if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
19661d05cddcSAtari911                    // Pre-render DokuWiki syntax to HTML for JavaScript display
19671d05cddcSAtari911                    $eventWithHtml = $event;
19681d05cddcSAtari911                    if (isset($event['title'])) {
19691d05cddcSAtari911                        $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']);
19701d05cddcSAtari911                    }
19711d05cddcSAtari911                    if (isset($event['description'])) {
19721d05cddcSAtari911                        $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']);
19731d05cddcSAtari911                    }
19741d05cddcSAtari911                    $weekEvents[$dateKey][] = $eventWithHtml;
19751d05cddcSAtari911                }
19761d05cddcSAtari911
19771d05cddcSAtari911                // Categorize for detailed sections
19781d05cddcSAtari911                if ($dateKey === $todayStr) {
19791d05cddcSAtari911                    $todayEvents[] = array_merge($event, ['date' => $dateKey]);
19801d05cddcSAtari911                } elseif ($dateKey === $tomorrowStr) {
19811d05cddcSAtari911                    $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]);
19821d05cddcSAtari911                } else {
19831d05cddcSAtari911                    // Check if this is an important namespace
19841d05cddcSAtari911                    $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
19851d05cddcSAtari911                    $isImportant = false;
19861d05cddcSAtari911                    foreach ($importantNsList as $impNs) {
19871d05cddcSAtari911                        if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
19881d05cddcSAtari911                            $isImportant = true;
19891d05cddcSAtari911                            break;
19901d05cddcSAtari911                        }
19911d05cddcSAtari911                    }
19921d05cddcSAtari911
19931d05cddcSAtari911                    // Important events: this week but not today/tomorrow
19941d05cddcSAtari911                    if ($isImportant && $dateKey >= $weekStart && $dateKey <= $weekEnd) {
19951d05cddcSAtari911                        $importantEvents[] = array_merge($event, ['date' => $dateKey]);
19961d05cddcSAtari911                    }
19971d05cddcSAtari911                }
19981d05cddcSAtari911            }
19991d05cddcSAtari911        }
20001d05cddcSAtari911
20011d05cddcSAtari911        // Start building HTML - Dynamic width with default font
20021d05cddcSAtari911        $html = '<div class="sidebar-widget sidebar-matrix" style="width:100%; max-width:100%; box-sizing:border-box; font-family:system-ui, sans-serif; background:#242424; border:2px solid #00cc07; border-radius:4px; overflow:hidden; box-shadow:0 0 10px rgba(0, 204, 7, 0.3);">';
20031d05cddcSAtari911
20041d05cddcSAtari911        // Sanitize calId for use in JavaScript variable names (remove dashes)
20051d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
20061d05cddcSAtari911
20071d05cddcSAtari911        // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it
20081d05cddcSAtari911        $html .= '<script>
20091d05cddcSAtari911(function() {
20101d05cddcSAtari911    // Shared state for system stats and tooltips
20111d05cddcSAtari911    const sharedState_' . $jsCalId . ' = {
20121d05cddcSAtari911        latestStats: {
20131d05cddcSAtari911            load: {"1min": 0, "5min": 0, "15min": 0},
20141d05cddcSAtari911            uptime: "",
20151d05cddcSAtari911            memory_details: {},
20161d05cddcSAtari911            top_processes: []
20171d05cddcSAtari911        },
20181d05cddcSAtari911        cpuHistory: [],
20191d05cddcSAtari911        CPU_HISTORY_SIZE: 2
20201d05cddcSAtari911    };
20211d05cddcSAtari911
20221d05cddcSAtari911    // Tooltip functions - MUST be defined before HTML uses them
20231d05cddcSAtari911    window["showTooltip_' . $jsCalId . '"] = function(color) {
20241d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
20251d05cddcSAtari911        if (!tooltip) {
20261d05cddcSAtari911            console.log("Tooltip element not found for color:", color);
20271d05cddcSAtari911            return;
20281d05cddcSAtari911        }
20291d05cddcSAtari911
20301d05cddcSAtari911        const latestStats = sharedState_' . $jsCalId . '.latestStats;
20311d05cddcSAtari911        let content = "";
20321d05cddcSAtari911
20331d05cddcSAtari911        if (color === "green") {
20341d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">CPU Load Average</div>";
20351d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
20361d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
20371d05cddcSAtari911            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
20381d05cddcSAtari911            if (latestStats.uptime) {
20391d05cddcSAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\\">Uptime: " + latestStats.uptime + "</div>";
20401d05cddcSAtari911            }
20411d05cddcSAtari911            tooltip.style.borderColor = "#00cc07";
20421d05cddcSAtari911            tooltip.style.color = "#00cc07";
20431d05cddcSAtari911        } else if (color === "purple") {
20441d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">CPU Load (Short-term)</div>";
20451d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
20461d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
20471d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
20481d05cddcSAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(155,89,182,0.3);\\" class=\\"tooltip-title\\">Top Processes</div>";
20491d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
20501d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
20511d05cddcSAtari911                });
20521d05cddcSAtari911            }
20531d05cddcSAtari911            tooltip.style.borderColor = "#9b59b6";
20541d05cddcSAtari911            tooltip.style.color = "#9b59b6";
20551d05cddcSAtari911        } else if (color === "orange") {
20561d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">Memory Usage</div>";
20571d05cddcSAtari911            if (latestStats.memory_details && latestStats.memory_details.total) {
20581d05cddcSAtari911                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
20591d05cddcSAtari911                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
20601d05cddcSAtari911                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
20611d05cddcSAtari911                if (latestStats.memory_details.cached) {
20621d05cddcSAtari911                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
20631d05cddcSAtari911                }
20641d05cddcSAtari911            } else {
20651d05cddcSAtari911                content += "<div>Loading...</div>";
20661d05cddcSAtari911            }
20671d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
20681d05cddcSAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(255,140,0,0.3);\\" class=\\"tooltip-title\\">Top Processes</div>";
20691d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
20701d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
20711d05cddcSAtari911                });
20721d05cddcSAtari911            }
20731d05cddcSAtari911            tooltip.style.borderColor = "#ff9800";
20741d05cddcSAtari911            tooltip.style.color = "#ff9800";
20751d05cddcSAtari911        }
20761d05cddcSAtari911
20771d05cddcSAtari911        tooltip.innerHTML = content;
20781d05cddcSAtari911        tooltip.style.display = "block";
20791d05cddcSAtari911
20801d05cddcSAtari911        const bar = tooltip.parentElement;
20811d05cddcSAtari911        const barRect = bar.getBoundingClientRect();
20821d05cddcSAtari911        const tooltipRect = tooltip.getBoundingClientRect();
20831d05cddcSAtari911
20841d05cddcSAtari911        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
20851d05cddcSAtari911        const top = barRect.top - tooltipRect.height - 8;
20861d05cddcSAtari911
20871d05cddcSAtari911        tooltip.style.left = left + "px";
20881d05cddcSAtari911        tooltip.style.top = top + "px";
20891d05cddcSAtari911    };
20901d05cddcSAtari911
20911d05cddcSAtari911    window["hideTooltip_' . $jsCalId . '"] = function(color) {
20921d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
20931d05cddcSAtari911        if (tooltip) {
20941d05cddcSAtari911            tooltip.style.display = "none";
20951d05cddcSAtari911        }
20961d05cddcSAtari911    };
20971d05cddcSAtari911
20981d05cddcSAtari911    // Update clock every second
20991d05cddcSAtari911    function updateClock() {
21001d05cddcSAtari911        const now = new Date();
21011d05cddcSAtari911        let hours = now.getHours();
21021d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
21031d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
21041d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
21051d05cddcSAtari911        hours = hours % 12 || 12;
21061d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
21071d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
21081d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
21091d05cddcSAtari911    }
21101d05cddcSAtari911    setInterval(updateClock, 1000);
21111d05cddcSAtari911
21121d05cddcSAtari911    // Weather update function
21131d05cddcSAtari911    function updateWeather() {
21141d05cddcSAtari911        if ("geolocation" in navigator) {
21151d05cddcSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
21161d05cddcSAtari911                const lat = position.coords.latitude;
21171d05cddcSAtari911                const lon = position.coords.longitude;
21181d05cddcSAtari911
21191d05cddcSAtari911                fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&temperature_unit=fahrenheit`)
21201d05cddcSAtari911                    .then(response => response.json())
21211d05cddcSAtari911                    .then(data => {
21221d05cddcSAtari911                        if (data.current_weather) {
21231d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
21241d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
21251d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
21261d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
21271d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
21281d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
21291d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
21301d05cddcSAtari911                        }
21311d05cddcSAtari911                    })
21321d05cddcSAtari911                    .catch(error => console.log("Weather fetch error:", error));
21331d05cddcSAtari911            }, function(error) {
21341d05cddcSAtari911                // If geolocation fails, use default location (Irvine, CA)
21351d05cddcSAtari911                fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265&current_weather=true&temperature_unit=fahrenheit")
21361d05cddcSAtari911                    .then(response => response.json())
21371d05cddcSAtari911                    .then(data => {
21381d05cddcSAtari911                        if (data.current_weather) {
21391d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
21401d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
21411d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
21421d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
21431d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
21441d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
21451d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
21461d05cddcSAtari911                        }
21471d05cddcSAtari911                    })
21481d05cddcSAtari911                    .catch(err => console.log("Weather error:", err));
21491d05cddcSAtari911            });
21501d05cddcSAtari911        } else {
21511d05cddcSAtari911            // No geolocation, use default (Irvine, CA)
21521d05cddcSAtari911            fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265&current_weather=true&temperature_unit=fahrenheit")
21531d05cddcSAtari911                .then(response => response.json())
21541d05cddcSAtari911                .then(data => {
21551d05cddcSAtari911                    if (data.current_weather) {
21561d05cddcSAtari911                        const temp = Math.round(data.current_weather.temperature);
21571d05cddcSAtari911                        const weatherCode = data.current_weather.weathercode;
21581d05cddcSAtari911                        const icon = getWeatherIcon(weatherCode);
21591d05cddcSAtari911                        const iconEl = document.getElementById("weather-icon-' . $calId . '");
21601d05cddcSAtari911                        const tempEl = document.getElementById("weather-temp-' . $calId . '");
21611d05cddcSAtari911                        if (iconEl) iconEl.textContent = icon;
21621d05cddcSAtari911                        if (tempEl) tempEl.innerHTML = temp + "&deg;";
21631d05cddcSAtari911                    }
21641d05cddcSAtari911                })
21651d05cddcSAtari911                .catch(err => console.log("Weather error:", err));
21661d05cddcSAtari911        }
21671d05cddcSAtari911    }
21681d05cddcSAtari911
21691d05cddcSAtari911    function getWeatherIcon(code) {
21701d05cddcSAtari911        const icons = {
21711d05cddcSAtari911            0: "☀️", 1: "��️", 2: "⛅", 3: "☁️",
21721d05cddcSAtari911            45: "��️", 48: "��️", 51: "��️", 53: "��️", 55: "��️",
21731d05cddcSAtari911            61: "��️", 63: "��️", 65: "⛈️", 71: "��️", 73: "��️",
21741d05cddcSAtari911            75: "❄️", 77: "��️", 80: "��️", 81: "��️", 82: "⛈️",
21751d05cddcSAtari911            85: "��️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️"
21761d05cddcSAtari911        };
21771d05cddcSAtari911        return icons[code] || "��️";
21781d05cddcSAtari911    }
21791d05cddcSAtari911
21801d05cddcSAtari911    // Update weather immediately and every 10 minutes
21811d05cddcSAtari911    updateWeather();
21821d05cddcSAtari911    setInterval(updateWeather, 600000);
21831d05cddcSAtari911
21841d05cddcSAtari911    // Update system stats and tooltips data
21851d05cddcSAtari911    function updateSystemStats() {
21861d05cddcSAtari911        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
21871d05cddcSAtari911            .then(response => response.json())
21881d05cddcSAtari911            .then(data => {
21891d05cddcSAtari911                sharedState_' . $jsCalId . '.latestStats = {
21901d05cddcSAtari911                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
21911d05cddcSAtari911                    uptime: data.uptime || "",
21921d05cddcSAtari911                    memory_details: data.memory_details || {},
21931d05cddcSAtari911                    top_processes: data.top_processes || []
21941d05cddcSAtari911                };
21951d05cddcSAtari911
21961d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
21971d05cddcSAtari911                if (greenBar) {
21981d05cddcSAtari911                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
21991d05cddcSAtari911                }
22001d05cddcSAtari911
22011d05cddcSAtari911                sharedState_' . $jsCalId . '.cpuHistory.push(data.cpu);
22021d05cddcSAtari911                if (sharedState_' . $jsCalId . '.cpuHistory.length > sharedState_' . $jsCalId . '.CPU_HISTORY_SIZE) {
22031d05cddcSAtari911                    sharedState_' . $jsCalId . '.cpuHistory.shift();
22041d05cddcSAtari911                }
22051d05cddcSAtari911
22061d05cddcSAtari911                const cpuAverage = sharedState_' . $jsCalId . '.cpuHistory.reduce((sum, val) => sum + val, 0) / sharedState_' . $jsCalId . '.cpuHistory.length;
22071d05cddcSAtari911
22081d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
22091d05cddcSAtari911                if (cpuBar) {
22101d05cddcSAtari911                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
22111d05cddcSAtari911                }
22121d05cddcSAtari911
22131d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
22141d05cddcSAtari911                if (memBar) {
22151d05cddcSAtari911                    memBar.style.width = Math.min(100, data.memory) + "%";
22161d05cddcSAtari911                }
22171d05cddcSAtari911            })
22181d05cddcSAtari911            .catch(error => {
22191d05cddcSAtari911                console.log("System stats error:", error);
22201d05cddcSAtari911            });
22211d05cddcSAtari911    }
22221d05cddcSAtari911
22231d05cddcSAtari911    updateSystemStats();
22241d05cddcSAtari911    setInterval(updateSystemStats, 2000);
22251d05cddcSAtari911})();
22261d05cddcSAtari911</script>';
22271d05cddcSAtari911
22281d05cddcSAtari911        // NOW add the header HTML (after JavaScript is defined)
22291d05cddcSAtari911        $todayDate = new DateTime();
22301d05cddcSAtari911        $displayDate = $todayDate->format('D, M j, Y');
22311d05cddcSAtari911        $currentTime = $todayDate->format('g:i:s A');
22321d05cddcSAtari911
22331d05cddcSAtari911        $html .= '<div class="eventlist-today-header">';
22341d05cddcSAtari911        $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>';
22351d05cddcSAtari911        $html .= '<div class="eventlist-bottom-info">';
22361d05cddcSAtari911        $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '">--°</span></span>';
22371d05cddcSAtari911        $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>';
22381d05cddcSAtari911        $html .= '</div>';
22391d05cddcSAtari911
22401d05cddcSAtari911        // Three CPU/Memory bars (all update live)
22411d05cddcSAtari911        $html .= '<div class="eventlist-stats-container">';
22421d05cddcSAtari911
22431d05cddcSAtari911        // 5-minute load average (green, updates every 2 seconds)
22441d05cddcSAtari911        $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $jsCalId . '(\'green\')" onmouseout="hideTooltip_' . $jsCalId . '(\'green\')">';
22451d05cddcSAtari911        $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>';
22461d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
22471d05cddcSAtari911        $html .= '</div>';
22481d05cddcSAtari911
22491d05cddcSAtari911        // Real-time CPU (purple, updates with 5-sec average)
22501d05cddcSAtari911        $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'purple\')" onmouseout="hideTooltip_' . $jsCalId . '(\'purple\')">';
22511d05cddcSAtari911        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>';
22521d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
22531d05cddcSAtari911        $html .= '</div>';
22541d05cddcSAtari911
22551d05cddcSAtari911        // Real-time Memory (orange, updates)
22561d05cddcSAtari911        $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'orange\')" onmouseout="hideTooltip_' . $jsCalId . '(\'orange\')">';
22571d05cddcSAtari911        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>';
22581d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
22591d05cddcSAtari911        $html .= '</div>';
22601d05cddcSAtari911
22611d05cddcSAtari911        $html .= '</div>';
22621d05cddcSAtari911        $html .= '</div>';
22631d05cddcSAtari911
2264*231d0edbSAtari911        // Get today's date for default event date
2265*231d0edbSAtari911        $todayStr = date('Y-m-d');
2266*231d0edbSAtari911
2267*231d0edbSAtari911        // Thin dark green "Add Event" bar between header and week grid (zero margin, smaller text, text positioned higher)
2268*231d0edbSAtari911        $html .= '<div style="background:#006400; padding:0; margin:0; height:12px; line-height:10px; text-align:center; cursor:pointer; border-top:1px solid rgba(0, 100, 0, 0.3); border-bottom:1px solid rgba(0, 100, 0, 0.3); box-shadow:0 0 8px rgba(0, 100, 0, 0.4); transition:all 0.2s;" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\', \'' . $todayStr . '\');" onmouseover="this.style.background=\'#004d00\'; this.style.boxShadow=\'0 0 12px rgba(0, 100, 0, 0.6)\';" onmouseout="this.style.background=\'#006400\'; this.style.boxShadow=\'0 0 8px rgba(0, 100, 0, 0.4)\';">';
2269*231d0edbSAtari911        $html .= '<span style="color:#00ff00; font-size:8px; font-weight:700; letter-spacing:0.4px; font-family:system-ui, sans-serif; text-shadow:0 0 3px rgba(0, 255, 0, 0.5); position:relative; top:-1px;">+ ADD EVENT</span>';
22701d05cddcSAtari911        $html .= '</div>';
22711d05cddcSAtari911
22721d05cddcSAtari911        // Week grid (7 cells)
22731d05cddcSAtari911        $html .= $this->renderWeekGrid($weekEvents, $weekStart);
22741d05cddcSAtari911
22751d05cddcSAtari911        // Today section (orange)
22761d05cddcSAtari911        if (!empty($todayEvents)) {
22771d05cddcSAtari911            $html .= $this->renderSidebarSection('Today', $todayEvents, '#ff9800', $calId);
22781d05cddcSAtari911        }
22791d05cddcSAtari911
22801d05cddcSAtari911        // Tomorrow section (green)
22811d05cddcSAtari911        if (!empty($tomorrowEvents)) {
22821d05cddcSAtari911            $html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, '#4caf50', $calId);
22831d05cddcSAtari911        }
22841d05cddcSAtari911
22851d05cddcSAtari911        // Important events section (purple)
22861d05cddcSAtari911        if (!empty($importantEvents)) {
22871d05cddcSAtari911            $html .= $this->renderSidebarSection('Important Events', $importantEvents, '#9b59b6', $calId);
22881d05cddcSAtari911        }
22891d05cddcSAtari911
22901d05cddcSAtari911        $html .= '</div>';
22911d05cddcSAtari911
2292*231d0edbSAtari911        // Add event dialog for sidebar widget
2293*231d0edbSAtari911        $html .= $this->renderEventDialog($calId, $namespace);
2294*231d0edbSAtari911
22951d05cddcSAtari911        return $html;
22961d05cddcSAtari911    }
22971d05cddcSAtari911
22981d05cddcSAtari911    /**
22991d05cddcSAtari911     * Render compact week grid (7 cells with event bars) - Matrix themed with clickable days
23001d05cddcSAtari911     */
23011d05cddcSAtari911    private function renderWeekGrid($weekEvents, $weekStart) {
23021d05cddcSAtari911        // Generate unique ID for this calendar instance - sanitize for JavaScript
23031d05cddcSAtari911        $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8);
23041d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);  // Sanitize for JS variable names
23051d05cddcSAtari911
23061d05cddcSAtari911        $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:#1a3d1a; border-bottom:2px solid #00cc07;">';
23071d05cddcSAtari911
23081d05cddcSAtari911        $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
23091d05cddcSAtari911        $today = date('Y-m-d');
23101d05cddcSAtari911
23111d05cddcSAtari911        for ($i = 0; $i < 7; $i++) {
23121d05cddcSAtari911            $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days'));
23131d05cddcSAtari911            $dayNum = date('j', strtotime($date));
23141d05cddcSAtari911            $isToday = $date === $today;
23151d05cddcSAtari911
23161d05cddcSAtari911            $events = isset($weekEvents[$date]) ? $weekEvents[$date] : [];
23171d05cddcSAtari911            $eventCount = count($events);
23181d05cddcSAtari911
23191d05cddcSAtari911            $bgColor = $isToday ? '#2a4d2a' : '#242424';
23201d05cddcSAtari911            $textColor = $isToday ? '#00ff00' : '#00cc07';
23211d05cddcSAtari911            $fontWeight = $isToday ? '700' : '500';
23221d05cddcSAtari911            $textShadow = $isToday ? 'text-shadow:0 0 6px rgba(0, 255, 0, 0.6);' : 'text-shadow:0 0 4px rgba(0, 204, 7, 0.4);';
23231d05cddcSAtari911
23241d05cddcSAtari911            $hasEvents = $eventCount > 0;
23251d05cddcSAtari911            $clickableStyle = $hasEvents ? 'cursor:pointer;' : '';
23261d05cddcSAtari911            $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : '';
23271d05cddcSAtari911
23281d05cddcSAtari911            $html .= '<div style="background:' . $bgColor . '; padding:4px 2px; text-align:center; min-height:45px; position:relative; border:1px solid rgba(0, 204, 7, 0.2); ' . $clickableStyle . '" ' . $clickHandler . '>';
23291d05cddcSAtari911
23301d05cddcSAtari911            // Day letter
23311d05cddcSAtari911            $html .= '<div style="font-size:9px; color:#00cc07; font-weight:500; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNames[$i] . '</div>';
23321d05cddcSAtari911
23331d05cddcSAtari911            // Day number
23341d05cddcSAtari911            $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>';
23351d05cddcSAtari911
23361d05cddcSAtari911            // Event bars (max 3 visible) with glow effect
23371d05cddcSAtari911            if ($eventCount > 0) {
23381d05cddcSAtari911                $showCount = min($eventCount, 3);
23391d05cddcSAtari911                for ($j = 0; $j < $showCount; $j++) {
23401d05cddcSAtari911                    $event = $events[$j];
23411d05cddcSAtari911                    $color = isset($event['color']) ? $event['color'] : '#00cc07';
23421d05cddcSAtari911                    $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:0 0 3px ' . htmlspecialchars($color) . ';"></div>';
23431d05cddcSAtari911                }
23441d05cddcSAtari911
23451d05cddcSAtari911                // Show "+N more" if more than 3
23461d05cddcSAtari911                if ($eventCount > 3) {
23471d05cddcSAtari911                    $html .= '<div style="font-size:7px; color:#00cc07; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 3) . '</div>';
23481d05cddcSAtari911                }
23491d05cddcSAtari911            }
23501d05cddcSAtari911
23511d05cddcSAtari911            $html .= '</div>';
23521d05cddcSAtari911        }
23531d05cddcSAtari911
23541d05cddcSAtari911        $html .= '</div>';
23551d05cddcSAtari911
23561d05cddcSAtari911        // Add container for selected day events display (with unique ID)
23571d05cddcSAtari911        $html .= '<div id="selected-day-events-' . $calId . '" style="display:none; margin:8px 4px; border-left:3px solid #3498db; box-shadow:0 0 5px rgba(0, 204, 7, 0.2);">';
23581d05cddcSAtari911        $html .= '<div style="background:#3498db; color:#000; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:0 0 8px #3498db; display:flex; justify-content:space-between; align-items:center;">';
23591d05cddcSAtari911        $html .= '<span id="selected-day-title-' . $calId . '"></span>';
23601d05cddcSAtari911        $html .= '<span onclick="document.getElementById(\'selected-day-events-' . $calId . '\').style.display=\'none\';" style="cursor:pointer; font-size:12px; padding:0 4px; font-weight:700;">✕</span>';
23611d05cddcSAtari911        $html .= '</div>';
23621d05cddcSAtari911        $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:rgba(36, 36, 36, 0.5);"></div>';
23631d05cddcSAtari911        $html .= '</div>';
23641d05cddcSAtari911
23651d05cddcSAtari911        // Add JavaScript for day selection with event data
23661d05cddcSAtari911        $html .= '<script>';
23671d05cddcSAtari911        // Sanitize calId for JavaScript variable names
23681d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
23691d05cddcSAtari911        $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';';
23701d05cddcSAtari911        $html .= '
23711d05cddcSAtari911        window.showDayEvents_' . $jsCalId . ' = function(dateKey) {
23721d05cddcSAtari911            const eventsData = window.weekEventsData_' . $jsCalId . ';
23731d05cddcSAtari911            const container = document.getElementById("selected-day-events-' . $calId . '");
23741d05cddcSAtari911            const title = document.getElementById("selected-day-title-' . $calId . '");
23751d05cddcSAtari911            const content = document.getElementById("selected-day-content-' . $calId . '");
23761d05cddcSAtari911
23771d05cddcSAtari911            if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return;
23781d05cddcSAtari911
23791d05cddcSAtari911            // Format date for display
23801d05cddcSAtari911            const dateObj = new Date(dateKey + "T00:00:00");
23811d05cddcSAtari911            const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" });
23821d05cddcSAtari911            const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" });
23831d05cddcSAtari911            title.textContent = dayName + ", " + monthDay;
23841d05cddcSAtari911
23851d05cddcSAtari911            // Clear content
23861d05cddcSAtari911            content.innerHTML = "";
23871d05cddcSAtari911
2388*231d0edbSAtari911            // Sort events by time (all-day events first, then timed events chronologically)
23891d05cddcSAtari911            const sortedEvents = [...eventsData[dateKey]].sort((a, b) => {
2390*231d0edbSAtari911                // All-day events (no time) go to the beginning
23911d05cddcSAtari911                if (!a.time && !b.time) return 0;
2392*231d0edbSAtari911                if (!a.time) return -1;  // a is all-day, comes first
2393*231d0edbSAtari911                if (!b.time) return 1;   // b is all-day, comes first
23941d05cddcSAtari911
23951d05cddcSAtari911                // Compare times (format: "HH:MM")
23961d05cddcSAtari911                const timeA = a.time.split(":").map(Number);
23971d05cddcSAtari911                const timeB = b.time.split(":").map(Number);
23981d05cddcSAtari911                const minutesA = timeA[0] * 60 + timeA[1];
23991d05cddcSAtari911                const minutesB = timeB[0] * 60 + timeB[1];
24001d05cddcSAtari911
24011d05cddcSAtari911                return minutesA - minutesB;
24021d05cddcSAtari911            });
24031d05cddcSAtari911
2404*231d0edbSAtari911            // Build events HTML with single color bar (event color only)
24051d05cddcSAtari911            sortedEvents.forEach(event => {
24061d05cddcSAtari911                const eventColor = event.color || "#00cc07";
24071d05cddcSAtari911
24081d05cddcSAtari911                const eventDiv = document.createElement("div");
2409*231d0edbSAtari911                eventDiv.style.cssText = "padding:4px 6px; border-bottom:1px solid rgba(0, 204, 7, 0.2); font-size:10px; display:flex; align-items:stretch; gap:6px; background:rgba(36, 36, 36, 0.3); min-height:20px;";
24101d05cddcSAtari911
24111d05cddcSAtari911                let eventHTML = "";
24121d05cddcSAtari911
2413*231d0edbSAtari911                // Event assigned color bar (single bar on left)
2414*231d0edbSAtari911                eventHTML += "<div style=\\"width:3px; align-self:stretch; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:0 0 3px " + eventColor + ";\\"></div>";
24151d05cddcSAtari911
2416*231d0edbSAtari911                // Content wrapper
2417*231d0edbSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0; display:flex; justify-content:space-between; align-items:start; gap:4px;\\">";
24181d05cddcSAtari911
2419*231d0edbSAtari911                // Left side: event details
24201d05cddcSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0;\\">";
24211d05cddcSAtari911                eventHTML += "<div style=\\"font-weight:600; color:#00cc07; word-wrap:break-word; font-family:system-ui, sans-serif; text-shadow:0 0 3px rgba(0, 204, 7, 0.4);\\">";
24221d05cddcSAtari911
24231d05cddcSAtari911                // Time
24241d05cddcSAtari911                if (event.time) {
24251d05cddcSAtari911                    const timeParts = event.time.split(":");
24261d05cddcSAtari911                    let hours = parseInt(timeParts[0]);
24271d05cddcSAtari911                    const minutes = timeParts[1];
24281d05cddcSAtari911                    const ampm = hours >= 12 ? "PM" : "AM";
24291d05cddcSAtari911                    hours = hours % 12 || 12;
24301d05cddcSAtari911                    eventHTML += "<span style=\\"color:#00dd00; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> ";
24311d05cddcSAtari911                }
24321d05cddcSAtari911
24331d05cddcSAtari911                // Title - use HTML version if available
24341d05cddcSAtari911                const titleHTML = event.title_html || event.title || "Untitled";
24351d05cddcSAtari911                eventHTML += titleHTML;
24361d05cddcSAtari911                eventHTML += "</div>";
24371d05cddcSAtari911
24381d05cddcSAtari911                // Description if present - use HTML version
24391d05cddcSAtari911                if (event.description_html || event.description) {
24401d05cddcSAtari911                    const descHTML = event.description_html || event.description;
24411d05cddcSAtari911                    eventHTML += "<div style=\\"font-size:9px; color:#00aa00; margin-top:2px;\\">" + descHTML + "</div>";
24421d05cddcSAtari911                }
24431d05cddcSAtari911
2444*231d0edbSAtari911                eventHTML += "</div>"; // Close event details
2445*231d0edbSAtari911
2446*231d0edbSAtari911                // Right side: conflict badge (if present)
2447*231d0edbSAtari911                if (event.conflict) {
2448*231d0edbSAtari911                    eventHTML += "<div style=\\"flex-shrink:0; color:#ff9800; font-size:10px; margin-top:2px; opacity:0.8;\\" title=\\"Time conflict detected\\">⚠</div>";
2449*231d0edbSAtari911                }
2450*231d0edbSAtari911
2451*231d0edbSAtari911                eventHTML += "</div>"; // Close content wrapper
24521d05cddcSAtari911
24531d05cddcSAtari911                eventDiv.innerHTML = eventHTML;
24541d05cddcSAtari911                content.appendChild(eventDiv);
24551d05cddcSAtari911            });
24561d05cddcSAtari911
24571d05cddcSAtari911            container.style.display = "block";
24581d05cddcSAtari911        };
24591d05cddcSAtari911        ';
24601d05cddcSAtari911        $html .= '</script>';
24611d05cddcSAtari911
24621d05cddcSAtari911        return $html;
24631d05cddcSAtari911    }
24641d05cddcSAtari911
24651d05cddcSAtari911    /**
24661d05cddcSAtari911     * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders
24671d05cddcSAtari911     */
24681d05cddcSAtari911    private function renderSidebarSection($title, $events, $accentColor, $calId) {
24691d05cddcSAtari911        // Keep the original accent colors for borders
24701d05cddcSAtari911        $borderColor = $accentColor;
24711d05cddcSAtari911
24721d05cddcSAtari911        // Show date for Important Events section
24731d05cddcSAtari911        $showDate = ($title === 'Important Events');
24741d05cddcSAtari911
24751d05cddcSAtari911        $html = '<div style="border-left:3px solid ' . $borderColor . '; margin:8px 4px; box-shadow:0 0 5px rgba(0, 204, 7, 0.2);">';
24761d05cddcSAtari911
24771d05cddcSAtari911        // Section header with accent color background - smaller, not all caps
24781d05cddcSAtari911        $html .= '<div style="background:' . $accentColor . '; color:#000; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:0 0 8px ' . $accentColor . ';">';
24791d05cddcSAtari911        $html .= htmlspecialchars($title);
24801d05cddcSAtari911        $html .= '</div>';
24811d05cddcSAtari911
24821d05cddcSAtari911        // Events
24831d05cddcSAtari911        $html .= '<div style="padding:4px 0; background:rgba(36, 36, 36, 0.5);">';
24841d05cddcSAtari911
24851d05cddcSAtari911        foreach ($events as $event) {
24861d05cddcSAtari911            $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor);
24871d05cddcSAtari911        }
24881d05cddcSAtari911
24891d05cddcSAtari911        $html .= '</div>';
24901d05cddcSAtari911        $html .= '</div>';
24911d05cddcSAtari911
24921d05cddcSAtari911        return $html;
24931d05cddcSAtari911    }
24941d05cddcSAtari911
24951d05cddcSAtari911    /**
24961d05cddcSAtari911     * Render individual event in sidebar - Matrix themed with dual color bars
24971d05cddcSAtari911     */
24981d05cddcSAtari911    private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07') {
24991d05cddcSAtari911        $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
25001d05cddcSAtari911        $time = isset($event['time']) ? $event['time'] : '';
25011d05cddcSAtari911        $endTime = isset($event['endTime']) ? $event['endTime'] : '';
25021d05cddcSAtari911        $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : '#00cc07';
25031d05cddcSAtari911        $date = isset($event['date']) ? $event['date'] : '';
25041d05cddcSAtari911        $isTask = isset($event['isTask']) && $event['isTask'];
25051d05cddcSAtari911        $completed = isset($event['completed']) && $event['completed'];
25061d05cddcSAtari911
25071d05cddcSAtari911        // Check for conflicts
25081d05cddcSAtari911        $hasConflict = isset($event['conflicts']) && !empty($event['conflicts']);
25091d05cddcSAtari911
2510*231d0edbSAtari911        $html = '<div style="padding:4px 6px; border-bottom:1px solid rgba(0, 204, 7, 0.2); font-size:10px; display:flex; align-items:stretch; gap:6px; background:rgba(36, 36, 36, 0.3); min-height:20px;">';
25111d05cddcSAtari911
2512*231d0edbSAtari911        // Event's assigned color bar (single bar on the left)
2513*231d0edbSAtari911        $html .= '<div style="width:3px; align-self:stretch; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:0 0 3px ' . $eventColor . ';"></div>';
25141d05cddcSAtari911
25151d05cddcSAtari911        // Content
25161d05cddcSAtari911        $html .= '<div style="flex:1; min-width:0;">';
25171d05cddcSAtari911
25181d05cddcSAtari911        // Time + title
25191d05cddcSAtari911        $html .= '<div style="font-weight:600; color:#00cc07; word-wrap:break-word; font-family:system-ui, sans-serif; text-shadow:0 0 3px rgba(0, 204, 7, 0.4);">';
25201d05cddcSAtari911
25211d05cddcSAtari911        if ($time) {
25221d05cddcSAtari911            $displayTime = $this->formatTimeDisplay($time, $endTime);
25231d05cddcSAtari911            $html .= '<span style="color:#00dd00; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> ';
25241d05cddcSAtari911        }
25251d05cddcSAtari911
25261d05cddcSAtari911        // Task checkbox
25271d05cddcSAtari911        if ($isTask) {
25281d05cddcSAtari911            $checkIcon = $completed ? '☑' : '☐';
25291d05cddcSAtari911            $html .= '<span style="font-size:11px; color:#00ff00;">' . $checkIcon . '</span> ';
25301d05cddcSAtari911        }
25311d05cddcSAtari911
25321d05cddcSAtari911        $html .= htmlspecialchars($title);
25331d05cddcSAtari911
25341d05cddcSAtari911        // Conflict badge
25351d05cddcSAtari911        if ($hasConflict) {
25361d05cddcSAtari911            $conflictCount = count($event['conflicts']);
25371d05cddcSAtari911            $html .= ' <span style="background:#ff0000; color:#000; padding:1px 3px; border-radius:2px; font-size:8px; font-weight:700; box-shadow:0 0 4px #ff0000;">⚠ ' . $conflictCount . '</span>';
25381d05cddcSAtari911        }
25391d05cddcSAtari911
25401d05cddcSAtari911        $html .= '</div>';
25411d05cddcSAtari911
25421d05cddcSAtari911        // Date display BELOW event name for Important events
25431d05cddcSAtari911        if ($showDate && $date) {
25441d05cddcSAtari911            $dateObj = new DateTime($date);
25451d05cddcSAtari911            $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10"
25461d05cddcSAtari911            $html .= '<div style="font-size:8px; color:#00aa00; font-weight:500; margin-top:2px; text-shadow:0 0 2px rgba(0, 170, 0, 0.3);">' . htmlspecialchars($displayDate) . '</div>';
25471d05cddcSAtari911        }
25481d05cddcSAtari911
25491d05cddcSAtari911        $html .= '</div>';
25501d05cddcSAtari911        $html .= '</div>';
25511d05cddcSAtari911
25521d05cddcSAtari911        return $html;
25531d05cddcSAtari911    }
25541d05cddcSAtari911
25551d05cddcSAtari911    /**
25561d05cddcSAtari911     * Format time display (12-hour format with optional end time)
25571d05cddcSAtari911     */
25581d05cddcSAtari911    private function formatTimeDisplay($startTime, $endTime = '') {
25591d05cddcSAtari911        // Convert start time
25601d05cddcSAtari911        list($hour, $minute) = explode(':', $startTime);
25611d05cddcSAtari911        $hour = (int)$hour;
25621d05cddcSAtari911        $ampm = $hour >= 12 ? 'PM' : 'AM';
25631d05cddcSAtari911        $displayHour = $hour % 12;
25641d05cddcSAtari911        if ($displayHour === 0) $displayHour = 12;
25651d05cddcSAtari911
25661d05cddcSAtari911        $display = $displayHour . ':' . $minute . ' ' . $ampm;
25671d05cddcSAtari911
25681d05cddcSAtari911        // Add end time if provided
25691d05cddcSAtari911        if ($endTime && $endTime !== '') {
25701d05cddcSAtari911            list($endHour, $endMinute) = explode(':', $endTime);
25711d05cddcSAtari911            $endHour = (int)$endHour;
25721d05cddcSAtari911            $endAmpm = $endHour >= 12 ? 'PM' : 'AM';
25731d05cddcSAtari911            $endDisplayHour = $endHour % 12;
25741d05cddcSAtari911            if ($endDisplayHour === 0) $endDisplayHour = 12;
25751d05cddcSAtari911
25761d05cddcSAtari911            $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm;
25771d05cddcSAtari911        }
25781d05cddcSAtari911
25791d05cddcSAtari911        return $display;
25801d05cddcSAtari911    }
25811d05cddcSAtari911
25821d05cddcSAtari911    /**
25831d05cddcSAtari911     * Render DokuWiki syntax to HTML
25841d05cddcSAtari911     * Converts **bold**, //italic//, [[links]], etc. to HTML
25851d05cddcSAtari911     */
25861d05cddcSAtari911    private function renderDokuWikiToHtml($text) {
25871d05cddcSAtari911        if (empty($text)) return '';
25881d05cddcSAtari911
25891d05cddcSAtari911        // Use DokuWiki's parser to render the text
25901d05cddcSAtari911        $instructions = p_get_instructions($text);
25911d05cddcSAtari911
25921d05cddcSAtari911        // Render instructions to XHTML
25931d05cddcSAtari911        $xhtml = p_render('xhtml', $instructions, $info);
25941d05cddcSAtari911
25951d05cddcSAtari911        // Remove surrounding <p> tags if present (we're rendering inline)
25961d05cddcSAtari911        $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml));
25971d05cddcSAtari911
25981d05cddcSAtari911        return $xhtml;
25991d05cddcSAtari911    }
26001d05cddcSAtari911
26011d05cddcSAtari911    // Keep old scanForNamespaces for backward compatibility (not used anymore)
26021d05cddcSAtari911    private function scanForNamespaces($dir, $baseNamespace, &$namespaces) {
26031d05cddcSAtari911        if (!is_dir($dir)) return;
26041d05cddcSAtari911
26051d05cddcSAtari911        $items = scandir($dir);
26061d05cddcSAtari911        foreach ($items as $item) {
26071d05cddcSAtari911            if ($item === '.' || $item === '..' || $item === 'calendar') continue;
26081d05cddcSAtari911
26091d05cddcSAtari911            $path = $dir . $item;
26101d05cddcSAtari911            if (is_dir($path)) {
26111d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
26121d05cddcSAtari911                $namespaces[] = $namespace;
26131d05cddcSAtari911                $this->scanForNamespaces($path . '/', $namespace, $namespaces);
26141d05cddcSAtari911            }
26151d05cddcSAtari911        }
26161d05cddcSAtari911    }
261719378907SAtari911}
2618