xref: /plugin/calendar/syntax.php (revision 1d05cddc261a22328c4671319b0963b94fa1a7e9)
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
116*1d05cddcSAtari911        $html = '<div class="calendar-compact-container" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '" data-year="' . $year . '" data-month="' . $month . '">';
117*1d05cddcSAtari911
118*1d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
119*1d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
120*1d05cddcSAtari911
121*1d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
122*1d05cddcSAtari911        $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
138*1d05cddcSAtari911        // Namespace filter indicator - only show if actively filtering a specific namespace
139*1d05cddcSAtari911        if ($namespace && $namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false) {
140*1d05cddcSAtari911            $html .= '<div class="calendar-namespace-filter" id="namespace-filter-' . $calId . '">';
141*1d05cddcSAtari911            $html .= '<span class="namespace-filter-label">Filtering:</span>';
142*1d05cddcSAtari911            $html .= '<span class="namespace-filter-name">' . htmlspecialchars($namespace) . '</span>';
143*1d05cddcSAtari911            $html .= '<button class="namespace-filter-clear" onclick="clearNamespaceFilter(\'' . $calId . '\')" title="Clear filter and show all namespaces">✕</button>';
144*1d05cddcSAtari911            $html .= '</div>';
145*1d05cddcSAtari911        }
146*1d05cddcSAtari911
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>';
288*1d05cddcSAtari911
289*1d05cddcSAtari911        // Search bar in header
290*1d05cddcSAtari911        $html .= '<div class="event-search-container-inline">';
291*1d05cddcSAtari911        $html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" placeholder="�� Search..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
292*1d05cddcSAtari911        $html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
293*1d05cddcSAtari911        $html .= '</div>';
294*1d05cddcSAtari911
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
320*1d05cddcSAtari911        // Check for time conflicts
321*1d05cddcSAtari911        $events = $this->checkTimeConflicts($events);
322*1d05cddcSAtari911
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) {
329*1d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
330*1d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
331*1d05cddcSAtari911
332*1d05cddcSAtari911                // All-day events (no time) go to the TOP
333*1d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
334*1d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
335*1d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
336*1d05cddcSAtari911
337*1d05cddcSAtari911                // 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
347*1d05cddcSAtari911        // Helper function to check if event is past (with 15-minute grace period for timed events)
348*1d05cddcSAtari911        $isEventPast = function($dateKey, $time) use ($today) {
349*1d05cddcSAtari911            // If event is on a past date, it's definitely past
350*1d05cddcSAtari911            if ($dateKey < $today) {
351*1d05cddcSAtari911                return true;
352*1d05cddcSAtari911            }
353*1d05cddcSAtari911
354*1d05cddcSAtari911            // If event is on a future date, it's definitely not past
355*1d05cddcSAtari911            if ($dateKey > $today) {
356*1d05cddcSAtari911                return false;
357*1d05cddcSAtari911            }
358*1d05cddcSAtari911
359*1d05cddcSAtari911            // Event is today - check time with grace period
360*1d05cddcSAtari911            if ($time && $time !== '') {
361*1d05cddcSAtari911                try {
362*1d05cddcSAtari911                    $currentDateTime = new DateTime();
363*1d05cddcSAtari911                    $eventDateTime = new DateTime($dateKey . ' ' . $time);
364*1d05cddcSAtari911
365*1d05cddcSAtari911                    // Add 15-minute grace period
366*1d05cddcSAtari911                    $eventDateTime->modify('+15 minutes');
367*1d05cddcSAtari911
368*1d05cddcSAtari911                    // Event is past if current time > event time + 15 minutes
369*1d05cddcSAtari911                    return $currentDateTime > $eventDateTime;
370*1d05cddcSAtari911                } catch (Exception $e) {
371*1d05cddcSAtari911                    // If time parsing fails, fall back to date-only comparison
372*1d05cddcSAtari911                    return false;
373*1d05cddcSAtari911                }
374*1d05cddcSAtari911            }
375*1d05cddcSAtari911
376*1d05cddcSAtari911            // No time specified for today's event, treat as future
377*1d05cddcSAtari911            return false;
378*1d05cddcSAtari911        };
379*1d05cddcSAtari911
380*1d05cddcSAtari911        // Build HTML for each event - separate past/completed from future
381*1d05cddcSAtari911        $pastHtml = '';
382*1d05cddcSAtari911        $futureHtml = '';
383*1d05cddcSAtari911        $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';
394*1d05cddcSAtari911                $timeRaw = isset($event['time']) ? $event['time'] : '';
395*1d05cddcSAtari911                $time = htmlspecialchars($timeRaw);
396*1d05cddcSAtari911                $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
403*1d05cddcSAtari911                // Use helper function to determine if event is past (with grace period)
404*1d05cddcSAtari911                $isPast = $isEventPast($dateKey, $timeRaw);
405*1d05cddcSAtari911                $isToday = $dateKey === $today;
406*1d05cddcSAtari911
407*1d05cddcSAtari911                // Check if event should be in past section
408*1d05cddcSAtari911                // EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past
409*1d05cddcSAtari911                $isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed;
410*1d05cddcSAtari911                if ($isPastOrCompleted) {
411*1d05cddcSAtari911                    $pastCount++;
412*1d05cddcSAtari911                }
413*1d05cddcSAtari911
414*1d05cddcSAtari911                // Determine if task is past due (past date, is task, not completed)
415*1d05cddcSAtari911                $isPastDue = $isPast && $isTask && !$completed;
416*1d05cddcSAtari911
41719378907SAtari911                // Process description for wiki syntax, HTML, images, and links
41819378907SAtari911                $renderedDescription = $this->renderDescription($description);
41919378907SAtari911
420*1d05cddcSAtari911                // 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');
426*1d05cddcSAtari911
427*1d05cddcSAtari911                        // Add end time if present and different from start time
428*1d05cddcSAtari911                        if ($endTime && $endTime !== $time) {
429*1d05cddcSAtari911                            $endTimeObj = DateTime::createFromFormat('H:i', $endTime);
430*1d05cddcSAtari911                            if ($endTimeObj) {
431*1d05cddcSAtari911                                $displayTime .= ' - ' . $endTimeObj->format('g:i A');
432*1d05cddcSAtari911                            }
433*1d05cddcSAtari911                        }
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' : '';
453*1d05cddcSAtari911                // Don't grey out past due tasks - they need attention!
454*1d05cddcSAtari911                $pastClass = ($isPast && !$isPastDue) ? ' event-past' : '';
455*1d05cddcSAtari911                $pastDueClass = $isPastDue ? ' event-pastdue' : '';
456e3a9f44cSAtari911                $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : '';
45719378907SAtari911
458*1d05cddcSAtari911                $eventHtml = '<div class="event-compact-item' . $completedClass . $pastClass . $pastDueClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';"' . $firstFutureAttr . '>';
45919378907SAtari911
460*1d05cddcSAtari911                $eventHtml .= '<div class="event-info">';
461*1d05cddcSAtari911                $eventHtml .= '<div class="event-title-row">';
462*1d05cddcSAtari911                $eventHtml .= '<span class="event-title-compact">' . $title . '</span>';
463*1d05cddcSAtari911                $eventHtml .= '</div>';
46419378907SAtari911
465e3a9f44cSAtari911                // For past events, hide meta and description (collapsed)
466*1d05cddcSAtari911                // EXCEPTION: Past due tasks should show their details
467*1d05cddcSAtari911                if (!$isPast || $isPastDue) {
468*1d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact">';
469*1d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
47019378907SAtari911                    if ($displayTime) {
471*1d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
47219378907SAtari911                    }
473*1d05cddcSAtari911                    // Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks
474*1d05cddcSAtari911                    if ($isPastDue) {
475*1d05cddcSAtari911                        $eventHtml .= ' <span class="event-pastdue-badge">PAST DUE</span>';
476*1d05cddcSAtari911                    } elseif ($isToday) {
477*1d05cddcSAtari911                        $eventHtml .= ' <span class="event-today-badge">TODAY</span>';
478e3a9f44cSAtari911                    }
479*1d05cddcSAtari911                    // 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                    }
484*1d05cddcSAtari911                    // Show badge if namespace exists and is not empty
485*1d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
486*1d05cddcSAtari911                        $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                    }
488*1d05cddcSAtari911
489*1d05cddcSAtari911                    // Add conflict warning if event has time conflicts
490*1d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
491*1d05cddcSAtari911                        $conflictList = [];
492*1d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
493*1d05cddcSAtari911                            $conflictText = htmlspecialchars($conflict['title']);
494*1d05cddcSAtari911                            if (!empty($conflict['time'])) {
495*1d05cddcSAtari911                                // Format time range
496*1d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
497*1d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
498*1d05cddcSAtari911
499*1d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
500*1d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
501*1d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
502*1d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
503*1d05cddcSAtari911                                } else {
504*1d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
505*1d05cddcSAtari911                                }
506*1d05cddcSAtari911                            }
507*1d05cddcSAtari911                            $conflictList[] = $conflictText;
508*1d05cddcSAtari911                        }
509*1d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
510*1d05cddcSAtari911                        $conflictJson = htmlspecialchars(json_encode($conflictList), ENT_QUOTES, 'UTF-8');
511*1d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
512*1d05cddcSAtari911                    }
513*1d05cddcSAtari911
514*1d05cddcSAtari911                    $eventHtml .= '</span>';
515*1d05cddcSAtari911                    $eventHtml .= '</div>';
51619378907SAtari911
51719378907SAtari911                    if ($description) {
518*1d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>';
519*1d05cddcSAtari911                    }
520*1d05cddcSAtari911                } else {
521*1d05cddcSAtari911                    // Past events: render with display:none for click-to-expand
522*1d05cddcSAtari911                    $eventHtml .= '<div class="event-meta-compact" style="display:none;">';
523*1d05cddcSAtari911                    $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
524*1d05cddcSAtari911                    if ($displayTime) {
525*1d05cddcSAtari911                        $eventHtml .= ' • ' . $displayTime;
526*1d05cddcSAtari911                    }
527*1d05cddcSAtari911                    $eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
528*1d05cddcSAtari911                    if (!$eventNamespace && isset($event['_namespace'])) {
529*1d05cddcSAtari911                        $eventNamespace = $event['_namespace'];
530*1d05cddcSAtari911                    }
531*1d05cddcSAtari911                    if ($eventNamespace && $eventNamespace !== '') {
532*1d05cddcSAtari911                        $eventHtml .= ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($eventNamespace) . '\')" style="cursor:pointer;" title="Click to filter by this namespace">' . htmlspecialchars($eventNamespace) . '</span>';
533*1d05cddcSAtari911                    }
534*1d05cddcSAtari911
535*1d05cddcSAtari911                    // Add conflict warning if event has time conflicts
536*1d05cddcSAtari911                    if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
537*1d05cddcSAtari911                        $conflictList = [];
538*1d05cddcSAtari911                        foreach ($event['conflictsWith'] as $conflict) {
539*1d05cddcSAtari911                            $conflictText = htmlspecialchars($conflict['title']);
540*1d05cddcSAtari911                            if (!empty($conflict['time'])) {
541*1d05cddcSAtari911                                $startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
542*1d05cddcSAtari911                                $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
543*1d05cddcSAtari911
544*1d05cddcSAtari911                                if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
545*1d05cddcSAtari911                                    $endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
546*1d05cddcSAtari911                                    $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
547*1d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
548*1d05cddcSAtari911                                } else {
549*1d05cddcSAtari911                                    $conflictText .= ' (' . $startTimeFormatted . ')';
550*1d05cddcSAtari911                                }
551*1d05cddcSAtari911                            }
552*1d05cddcSAtari911                            $conflictList[] = $conflictText;
553*1d05cddcSAtari911                        }
554*1d05cddcSAtari911                        $conflictCount = count($event['conflictsWith']);
555*1d05cddcSAtari911                        $conflictJson = htmlspecialchars(json_encode($conflictList), ENT_QUOTES, 'UTF-8');
556*1d05cddcSAtari911                        $eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' . $conflictCount . '</span>';
557*1d05cddcSAtari911                    }
558*1d05cddcSAtari911
559*1d05cddcSAtari911                    $eventHtml .= '</span>';
560*1d05cddcSAtari911                    $eventHtml .= '</div>';
561*1d05cddcSAtari911
562*1d05cddcSAtari911                    if ($description) {
563*1d05cddcSAtari911                        $eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '</div>';
56419378907SAtari911                    }
565e3a9f44cSAtari911                }
56619378907SAtari911
567*1d05cddcSAtari911                $eventHtml .= '</div>'; // event-info
56819378907SAtari911
569e3a9f44cSAtari911                // Use stored namespace from event, fallback to passed namespace
570e3a9f44cSAtari911                $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace;
571e3a9f44cSAtari911
572*1d05cddcSAtari911                $eventHtml .= '<div class="event-actions-compact">';
573*1d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">��️</button>';
574*1d05cddcSAtari911                $eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>';
575*1d05cddcSAtari911                $eventHtml .= '</div>';
57619378907SAtari911
57719378907SAtari911                // Checkbox for tasks - ON THE FAR RIGHT
57819378907SAtari911                if ($isTask) {
57919378907SAtari911                    $checked = $completed ? 'checked' : '';
580*1d05cddcSAtari911                    $eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">';
58119378907SAtari911                }
58219378907SAtari911
583*1d05cddcSAtari911                $eventHtml .= '</div>';
584*1d05cddcSAtari911
585*1d05cddcSAtari911                // Add to appropriate section
586*1d05cddcSAtari911                if ($isPastOrCompleted) {
587*1d05cddcSAtari911                    $pastHtml .= $eventHtml;
588*1d05cddcSAtari911                } else {
589*1d05cddcSAtari911                    $futureHtml .= $eventHtml;
590*1d05cddcSAtari911                }
591*1d05cddcSAtari911            }
592*1d05cddcSAtari911        }
593*1d05cddcSAtari911
594*1d05cddcSAtari911        // Build final HTML with collapsible past events section
595*1d05cddcSAtari911        $html = '';
596*1d05cddcSAtari911
597*1d05cddcSAtari911        // Add collapsible past events section if any exist
598*1d05cddcSAtari911        if ($pastCount > 0) {
599*1d05cddcSAtari911            $html .= '<div class="past-events-section">';
600*1d05cddcSAtari911            $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">';
601*1d05cddcSAtari911            $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> ';
602*1d05cddcSAtari911            $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>';
60319378907SAtari911            $html .= '</div>';
604*1d05cddcSAtari911            $html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">';
605*1d05cddcSAtari911            $html .= $pastHtml;
606*1d05cddcSAtari911            $html .= '</div>';
607*1d05cddcSAtari911            $html .= '</div>';
608*1d05cddcSAtari911        }
609e3a9f44cSAtari911
610*1d05cddcSAtari911        // Add future events
611*1d05cddcSAtari911        $html .= $futureHtml;
61219378907SAtari911
61319378907SAtari911        return $html;
61419378907SAtari911    }
61519378907SAtari911
616*1d05cddcSAtari911    /**
617*1d05cddcSAtari911     * Check for time conflicts between events
618*1d05cddcSAtari911     */
619*1d05cddcSAtari911    private function checkTimeConflicts($events) {
620*1d05cddcSAtari911        // Group events by date
621*1d05cddcSAtari911        $eventsByDate = [];
622*1d05cddcSAtari911        foreach ($events as $date => $dateEvents) {
623*1d05cddcSAtari911            if (!is_array($dateEvents)) continue;
624*1d05cddcSAtari911
625*1d05cddcSAtari911            foreach ($dateEvents as $evt) {
626*1d05cddcSAtari911                if (empty($evt['time'])) continue; // Skip all-day events
627*1d05cddcSAtari911
628*1d05cddcSAtari911                if (!isset($eventsByDate[$date])) {
629*1d05cddcSAtari911                    $eventsByDate[$date] = [];
630*1d05cddcSAtari911                }
631*1d05cddcSAtari911                $eventsByDate[$date][] = $evt;
632*1d05cddcSAtari911            }
633*1d05cddcSAtari911        }
634*1d05cddcSAtari911
635*1d05cddcSAtari911        // Check for overlaps on each date
636*1d05cddcSAtari911        foreach ($eventsByDate as $date => $dateEvents) {
637*1d05cddcSAtari911            for ($i = 0; $i < count($dateEvents); $i++) {
638*1d05cddcSAtari911                for ($j = $i + 1; $j < count($dateEvents); $j++) {
639*1d05cddcSAtari911                    if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) {
640*1d05cddcSAtari911                        // Mark both events as conflicting
641*1d05cddcSAtari911                        $dateEvents[$i]['hasConflict'] = true;
642*1d05cddcSAtari911                        $dateEvents[$j]['hasConflict'] = true;
643*1d05cddcSAtari911
644*1d05cddcSAtari911                        // Store conflict info
645*1d05cddcSAtari911                        if (!isset($dateEvents[$i]['conflictsWith'])) {
646*1d05cddcSAtari911                            $dateEvents[$i]['conflictsWith'] = [];
647*1d05cddcSAtari911                        }
648*1d05cddcSAtari911                        if (!isset($dateEvents[$j]['conflictsWith'])) {
649*1d05cddcSAtari911                            $dateEvents[$j]['conflictsWith'] = [];
650*1d05cddcSAtari911                        }
651*1d05cddcSAtari911
652*1d05cddcSAtari911                        $dateEvents[$i]['conflictsWith'][] = [
653*1d05cddcSAtari911                            'id' => $dateEvents[$j]['id'],
654*1d05cddcSAtari911                            'title' => $dateEvents[$j]['title'],
655*1d05cddcSAtari911                            'time' => $dateEvents[$j]['time'],
656*1d05cddcSAtari911                            'endTime' => isset($dateEvents[$j]['endTime']) ? $dateEvents[$j]['endTime'] : ''
657*1d05cddcSAtari911                        ];
658*1d05cddcSAtari911
659*1d05cddcSAtari911                        $dateEvents[$j]['conflictsWith'][] = [
660*1d05cddcSAtari911                            'id' => $dateEvents[$i]['id'],
661*1d05cddcSAtari911                            'title' => $dateEvents[$i]['title'],
662*1d05cddcSAtari911                            'time' => $dateEvents[$i]['time'],
663*1d05cddcSAtari911                            'endTime' => isset($dateEvents[$i]['endTime']) ? $dateEvents[$i]['endTime'] : ''
664*1d05cddcSAtari911                        ];
665*1d05cddcSAtari911                    }
666*1d05cddcSAtari911                }
667*1d05cddcSAtari911            }
668*1d05cddcSAtari911
669*1d05cddcSAtari911            // Update the events array with conflict information
670*1d05cddcSAtari911            foreach ($events[$date] as &$evt) {
671*1d05cddcSAtari911                foreach ($dateEvents as $checkedEvt) {
672*1d05cddcSAtari911                    if ($evt['id'] === $checkedEvt['id']) {
673*1d05cddcSAtari911                        if (isset($checkedEvt['hasConflict'])) {
674*1d05cddcSAtari911                            $evt['hasConflict'] = $checkedEvt['hasConflict'];
675*1d05cddcSAtari911                        }
676*1d05cddcSAtari911                        if (isset($checkedEvt['conflictsWith'])) {
677*1d05cddcSAtari911                            $evt['conflictsWith'] = $checkedEvt['conflictsWith'];
678*1d05cddcSAtari911                        }
679*1d05cddcSAtari911                        break;
680*1d05cddcSAtari911                    }
681*1d05cddcSAtari911                }
682*1d05cddcSAtari911            }
683*1d05cddcSAtari911        }
684*1d05cddcSAtari911
685*1d05cddcSAtari911        return $events;
686*1d05cddcSAtari911    }
687*1d05cddcSAtari911
688*1d05cddcSAtari911    /**
689*1d05cddcSAtari911     * Check if two events overlap in time
690*1d05cddcSAtari911     */
691*1d05cddcSAtari911    private function eventsOverlap($evt1, $evt2) {
692*1d05cddcSAtari911        if (empty($evt1['time']) || empty($evt2['time'])) {
693*1d05cddcSAtari911            return false; // All-day events don't conflict
694*1d05cddcSAtari911        }
695*1d05cddcSAtari911
696*1d05cddcSAtari911        $start1 = $evt1['time'];
697*1d05cddcSAtari911        $end1 = isset($evt1['endTime']) && !empty($evt1['endTime']) ? $evt1['endTime'] : $evt1['time'];
698*1d05cddcSAtari911
699*1d05cddcSAtari911        $start2 = $evt2['time'];
700*1d05cddcSAtari911        $end2 = isset($evt2['endTime']) && !empty($evt2['endTime']) ? $evt2['endTime'] : $evt2['time'];
701*1d05cddcSAtari911
702*1d05cddcSAtari911        // Convert to minutes for easier comparison
703*1d05cddcSAtari911        $start1Mins = $this->timeToMinutes($start1);
704*1d05cddcSAtari911        $end1Mins = $this->timeToMinutes($end1);
705*1d05cddcSAtari911        $start2Mins = $this->timeToMinutes($start2);
706*1d05cddcSAtari911        $end2Mins = $this->timeToMinutes($end2);
707*1d05cddcSAtari911
708*1d05cddcSAtari911        // Check for overlap: start1 < end2 AND start2 < end1
709*1d05cddcSAtari911        return $start1Mins < $end2Mins && $start2Mins < $end1Mins;
710*1d05cddcSAtari911    }
711*1d05cddcSAtari911
712*1d05cddcSAtari911    /**
713*1d05cddcSAtari911     * Convert HH:MM time to minutes since midnight
714*1d05cddcSAtari911     */
715*1d05cddcSAtari911    private function timeToMinutes($timeStr) {
716*1d05cddcSAtari911        $parts = explode(':', $timeStr);
717*1d05cddcSAtari911        if (count($parts) !== 2) return 0;
718*1d05cddcSAtari911
719*1d05cddcSAtari911        return (int)$parts[0] * 60 + (int)$parts[1];
720*1d05cddcSAtari911    }
721*1d05cddcSAtari911
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
759*1d05cddcSAtari911        $html = '<div class="event-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namespace="' . htmlspecialchars($namespace) . '" data-original-namespace="' . htmlspecialchars($namespace) . '">';
76019378907SAtari911
761*1d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
762*1d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
763*1d05cddcSAtari911
764*1d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
765*1d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
766*1d05cddcSAtari911
767*1d05cddcSAtari911        // Compact two-row header designed for ~500px width
768*1d05cddcSAtari911        $html .= '<div class="panel-header-compact">';
769*1d05cddcSAtari911
770*1d05cddcSAtari911        // Row 1: Navigation and title
771*1d05cddcSAtari911        $html .= '<div class="panel-header-row-1">';
772*1d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
773*1d05cddcSAtari911
774*1d05cddcSAtari911        // Compact month name (e.g. "Feb 2026" instead of "February 2026 Events")
775*1d05cddcSAtari911        $shortMonthName = date('M Y', mktime(0, 0, 0, $month, 1, $year));
776*1d05cddcSAtari911        $html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $shortMonthName . '</h3>';
777*1d05cddcSAtari911
778*1d05cddcSAtari911        $html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
779*1d05cddcSAtari911
780*1d05cddcSAtari911        // Namespace badge (if applicable)
78187ac9bf3SAtari911        if ($namespace) {
782e3a9f44cSAtari911            if ($isMultiNamespace) {
783e3a9f44cSAtari911                if (strpos($namespace, '*') !== false) {
784*1d05cddcSAtari911                    $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>';
785e3a9f44cSAtari911                } else {
786e3a9f44cSAtari911                    $namespaceList = array_map('trim', explode(';', $namespace));
787*1d05cddcSAtari911                    $nsCount = count($namespaceList);
788*1d05cddcSAtari911                    $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars(implode(', ', $namespaceList)) . '">' . $nsCount . ' NS</span>';
789e3a9f44cSAtari911                }
790e3a9f44cSAtari911            } else {
791*1d05cddcSAtari911                $isFiltering = ($namespace !== '*' && strpos($namespace, '*') === false && strpos($namespace, ';') === false);
792*1d05cddcSAtari911                if ($isFiltering) {
793*1d05cddcSAtari911                    $html .= '<span class="panel-ns-badge filter-on" title="Filtering by ' . htmlspecialchars($namespace) . ' - click to clear" onclick="clearNamespaceFilterPanel(\'' . $calId . '\')">' . htmlspecialchars($namespace) . ' ✕</span>';
794*1d05cddcSAtari911                } else {
795*1d05cddcSAtari911                    $html .= '<span class="panel-ns-badge" title="' . htmlspecialchars($namespace) . '">' . htmlspecialchars($namespace) . '</span>';
79687ac9bf3SAtari911                }
797e3a9f44cSAtari911            }
798*1d05cddcSAtari911        }
799*1d05cddcSAtari911
800*1d05cddcSAtari911        $html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>';
80119378907SAtari911        $html .= '</div>';
80219378907SAtari911
803*1d05cddcSAtari911        // Row 2: Search and add button
804*1d05cddcSAtari911        $html .= '<div class="panel-header-row-2">';
805*1d05cddcSAtari911        $html .= '<div class="panel-search-box">';
806*1d05cddcSAtari911        $html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placeholder="Search events..." oninput="filterEvents(\'' . $calId . '\', this.value)">';
807*1d05cddcSAtari911        $html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventSearch(\'' . $calId . '\')" style="display:none;">✕</button>';
808*1d05cddcSAtari911        $html .= '</div>';
809*1d05cddcSAtari911        $html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
810*1d05cddcSAtari911        $html .= '</div>';
811*1d05cddcSAtari911
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'];
830*1d05cddcSAtari911        // If no namespace specified, show all namespaces
831*1d05cddcSAtari911        if (empty($namespace)) {
832*1d05cddcSAtari911            $namespace = '*';
833*1d05cddcSAtari911        }
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;
839*1d05cddcSAtari911        $showchecked = isset($data['showchecked']) ? true : false; // New parameter
840*1d05cddcSAtari911        $noheader = isset($data['noheader']) ? true : false; // New parameter to hide header
84119378907SAtari911
842e3a9f44cSAtari911        // Handle "range" parameter - day, week, or month
843e3a9f44cSAtari911        if ($range === 'day') {
844*1d05cddcSAtari911            $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') {
848*1d05cddcSAtari911            $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
849*1d05cddcSAtari911            $endDateTime = new DateTime();
850e3a9f44cSAtari911            $endDateTime->modify('+7 days');
851e3a9f44cSAtari911            $endDate = $endDateTime->format('Y-m-d');
852e3a9f44cSAtari911            $headerText = 'This Week';
853e3a9f44cSAtari911        } elseif ($range === 'month') {
854*1d05cddcSAtari911            $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
856*1d05cddcSAtari911            $dt = new DateTime();
857e3a9f44cSAtari911            $headerText = $dt->format('F Y');
858e3a9f44cSAtari911        } elseif ($sidebar) {
859*1d05cddcSAtari911            // NEW: Sidebar widget - load current week's events
860*1d05cddcSAtari911            $weekStart = date('Y-m-d', strtotime('monday this week'));
861*1d05cddcSAtari911            $weekEnd = date('Y-m-d', strtotime('sunday this week'));
862*1d05cddcSAtari911
863*1d05cddcSAtari911            // Load events for the entire week
864*1d05cddcSAtari911            $start = new DateTime($weekStart);
865*1d05cddcSAtari911            $end = new DateTime($weekEnd);
866*1d05cddcSAtari911            $end->modify('+1 day'); // DatePeriod excludes end date
867*1d05cddcSAtari911            $interval = new DateInterval('P1D');
868*1d05cddcSAtari911            $period = new DatePeriod($start, $interval, $end);
869*1d05cddcSAtari911
870*1d05cddcSAtari911            $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false);
871*1d05cddcSAtari911            $allEvents = [];
872*1d05cddcSAtari911            $loadedMonths = [];
873*1d05cddcSAtari911
874*1d05cddcSAtari911            foreach ($period as $dt) {
875*1d05cddcSAtari911                $year = (int)$dt->format('Y');
876*1d05cddcSAtari911                $month = (int)$dt->format('n');
877*1d05cddcSAtari911                $dateKey = $dt->format('Y-m-d');
878*1d05cddcSAtari911
879*1d05cddcSAtari911                $monthKey = $year . '-' . $month . '-' . $namespace;
880*1d05cddcSAtari911
881*1d05cddcSAtari911                if (!isset($loadedMonths[$monthKey])) {
882*1d05cddcSAtari911                    if ($isMultiNamespace) {
883*1d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month);
884*1d05cddcSAtari911                    } else {
885*1d05cddcSAtari911                        $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
886*1d05cddcSAtari911                    }
887*1d05cddcSAtari911                }
888*1d05cddcSAtari911
889*1d05cddcSAtari911                $monthEvents = $loadedMonths[$monthKey];
890*1d05cddcSAtari911
891*1d05cddcSAtari911                if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
892*1d05cddcSAtari911                    $allEvents[$dateKey] = $monthEvents[$dateKey];
893*1d05cddcSAtari911                }
894*1d05cddcSAtari911            }
895*1d05cddcSAtari911
896*1d05cddcSAtari911            // Apply time conflict detection
897*1d05cddcSAtari911            $allEvents = $this->checkTimeConflicts($allEvents);
898*1d05cddcSAtari911
899*1d05cddcSAtari911            $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8);
900*1d05cddcSAtari911
901*1d05cddcSAtari911            // Render sidebar widget and return immediately
902*1d05cddcSAtari911            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
960*1d05cddcSAtari911        // Sort events by date (already sorted by dateKey), then by time within each day
961*1d05cddcSAtari911        foreach ($allEvents as $dateKey => &$dayEvents) {
962*1d05cddcSAtari911            usort($dayEvents, function($a, $b) {
963*1d05cddcSAtari911                $timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
964*1d05cddcSAtari911                $timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
965*1d05cddcSAtari911
966*1d05cddcSAtari911                // All-day events (no time) go to the TOP
967*1d05cddcSAtari911                if ($timeA === null && $timeB !== null) return -1; // A before B
968*1d05cddcSAtari911                if ($timeA !== null && $timeB === null) return 1;  // A after B
969*1d05cddcSAtari911                if ($timeA === null && $timeB === null) return 0;  // Both all-day, equal
970*1d05cddcSAtari911
971*1d05cddcSAtari911                // Both have times, sort chronologically
972*1d05cddcSAtari911                return strcmp($timeA, $timeB);
973*1d05cddcSAtari911            });
974*1d05cddcSAtari911        }
975*1d05cddcSAtari911        unset($dayEvents); // Break reference
976*1d05cddcSAtari911
977e3a9f44cSAtari911        // Simple 2-line display widget
978*1d05cddcSAtari911        $calId = 'eventlist_' . uniqid();
979*1d05cddcSAtari911        $html = '<div class="eventlist-simple" id="' . $calId . '">';
980*1d05cddcSAtari911
981*1d05cddcSAtari911        // Load calendar JavaScript manually (not through DokuWiki concatenation)
982*1d05cddcSAtari911        $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
983*1d05cddcSAtari911
984*1d05cddcSAtari911        // Initialize DOKU_BASE for JavaScript
985*1d05cddcSAtari911        $html .= '<script>if(typeof DOKU_BASE==="undefined"){window.DOKU_BASE="' . DOKU_BASE . '";}</script>';
986*1d05cddcSAtari911
987*1d05cddcSAtari911        // Add compact header with date and clock for "today" mode (unless noheader is set)
988*1d05cddcSAtari911        if ($today && !empty($allEvents) && !$noheader) {
989*1d05cddcSAtari911            $todayDate = new DateTime();
990*1d05cddcSAtari911            $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026"
991*1d05cddcSAtari911            $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM"
992*1d05cddcSAtari911
993*1d05cddcSAtari911            $html .= '<div class="eventlist-today-header">';
994*1d05cddcSAtari911            $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>';
995*1d05cddcSAtari911            $html .= '<div class="eventlist-bottom-info">';
996*1d05cddcSAtari911            $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '">--°</span></span>';
997*1d05cddcSAtari911            $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>';
998*1d05cddcSAtari911            $html .= '</div>';
999*1d05cddcSAtari911
1000*1d05cddcSAtari911            // Three CPU/Memory bars (all update live)
1001*1d05cddcSAtari911            $html .= '<div class="eventlist-stats-container">';
1002*1d05cddcSAtari911
1003*1d05cddcSAtari911            // 5-minute load average (green, updates every 2 seconds)
1004*1d05cddcSAtari911            $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $calId . '(\'green\')" onmouseout="hideTooltip_' . $calId . '(\'green\')">';
1005*1d05cddcSAtari911            $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>';
1006*1d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
1007*1d05cddcSAtari911            $html .= '</div>';
1008*1d05cddcSAtari911
1009*1d05cddcSAtari911            // Real-time CPU (purple, updates with 5-sec average)
1010*1d05cddcSAtari911            $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $calId . '(\'purple\')" onmouseout="hideTooltip_' . $calId . '(\'purple\')">';
1011*1d05cddcSAtari911            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>';
1012*1d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
1013*1d05cddcSAtari911            $html .= '</div>';
1014*1d05cddcSAtari911
1015*1d05cddcSAtari911            // Real-time Memory (orange, updates)
1016*1d05cddcSAtari911            $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $calId . '(\'orange\')" onmouseout="hideTooltip_' . $calId . '(\'orange\')">';
1017*1d05cddcSAtari911            $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>';
1018*1d05cddcSAtari911            $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
1019*1d05cddcSAtari911            $html .= '</div>';
1020*1d05cddcSAtari911
1021*1d05cddcSAtari911            $html .= '</div>';
1022*1d05cddcSAtari911            $html .= '</div>';
1023*1d05cddcSAtari911
1024*1d05cddcSAtari911            // Add JavaScript to update clock and weather
1025*1d05cddcSAtari911            $html .= '<script>
1026*1d05cddcSAtari911(function() {
1027*1d05cddcSAtari911    // Update clock every second
1028*1d05cddcSAtari911    function updateClock() {
1029*1d05cddcSAtari911        const now = new Date();
1030*1d05cddcSAtari911        let hours = now.getHours();
1031*1d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
1032*1d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
1033*1d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
1034*1d05cddcSAtari911        hours = hours % 12 || 12;
1035*1d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
1036*1d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
1037*1d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
1038*1d05cddcSAtari911    }
1039*1d05cddcSAtari911    setInterval(updateClock, 1000);
1040*1d05cddcSAtari911
1041*1d05cddcSAtari911    // Fetch weather (geolocation-based)
1042*1d05cddcSAtari911    function updateWeather() {
1043*1d05cddcSAtari911        if ("geolocation" in navigator) {
1044*1d05cddcSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
1045*1d05cddcSAtari911                const lat = position.coords.latitude;
1046*1d05cddcSAtari911                const lon = position.coords.longitude;
1047*1d05cddcSAtari911
1048*1d05cddcSAtari911                // Use Open-Meteo API (free, no key required)
1049*1d05cddcSAtari911                fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&temperature_unit=fahrenheit`)
1050*1d05cddcSAtari911                    .then(response => response.json())
1051*1d05cddcSAtari911                    .then(data => {
1052*1d05cddcSAtari911                        if (data.current_weather) {
1053*1d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
1054*1d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
1055*1d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
1056*1d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
1057*1d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
1058*1d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
1059*1d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
1060*1d05cddcSAtari911                        }
1061*1d05cddcSAtari911                    })
1062*1d05cddcSAtari911                    .catch(error => {
1063*1d05cddcSAtari911                        console.log("Weather fetch error:", error);
1064*1d05cddcSAtari911                    });
1065*1d05cddcSAtari911            }, function(error) {
1066*1d05cddcSAtari911                // If geolocation fails, use Sacramento as default
1067*1d05cddcSAtari911                fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944&current_weather=true&temperature_unit=fahrenheit")
1068*1d05cddcSAtari911                    .then(response => response.json())
1069*1d05cddcSAtari911                    .then(data => {
1070*1d05cddcSAtari911                        if (data.current_weather) {
1071*1d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
1072*1d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
1073*1d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
1074*1d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
1075*1d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
1076*1d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
1077*1d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
1078*1d05cddcSAtari911                        }
1079*1d05cddcSAtari911                    })
1080*1d05cddcSAtari911                    .catch(err => console.log("Weather error:", err));
1081*1d05cddcSAtari911            });
1082*1d05cddcSAtari911        } else {
1083*1d05cddcSAtari911            // No geolocation, use Sacramento
1084*1d05cddcSAtari911            fetch("https://api.open-meteo.com/v1/forecast?latitude=38.5816&longitude=-121.4944&current_weather=true&temperature_unit=fahrenheit")
1085*1d05cddcSAtari911                .then(response => response.json())
1086*1d05cddcSAtari911                .then(data => {
1087*1d05cddcSAtari911                    if (data.current_weather) {
1088*1d05cddcSAtari911                        const temp = Math.round(data.current_weather.temperature);
1089*1d05cddcSAtari911                        const weatherCode = data.current_weather.weathercode;
1090*1d05cddcSAtari911                        const icon = getWeatherIcon(weatherCode);
1091*1d05cddcSAtari911                        const iconEl = document.getElementById("weather-icon-' . $calId . '");
1092*1d05cddcSAtari911                        const tempEl = document.getElementById("weather-temp-' . $calId . '");
1093*1d05cddcSAtari911                        if (iconEl) iconEl.textContent = icon;
1094*1d05cddcSAtari911                        if (tempEl) tempEl.innerHTML = temp + "&deg;";
1095*1d05cddcSAtari911                    }
1096*1d05cddcSAtari911                })
1097*1d05cddcSAtari911                .catch(err => console.log("Weather error:", err));
1098*1d05cddcSAtari911        }
1099*1d05cddcSAtari911    }
1100*1d05cddcSAtari911
1101*1d05cddcSAtari911    // WMO Weather interpretation codes
1102*1d05cddcSAtari911    function getWeatherIcon(code) {
1103*1d05cddcSAtari911        const icons = {
1104*1d05cddcSAtari911            0: "☀️",   // Clear sky
1105*1d05cddcSAtari911            1: "��️",   // Mainly clear
1106*1d05cddcSAtari911            2: "⛅",   // Partly cloudy
1107*1d05cddcSAtari911            3: "☁️",   // Overcast
1108*1d05cddcSAtari911            45: "��️",  // Fog
1109*1d05cddcSAtari911            48: "��️",  // Depositing rime fog
1110*1d05cddcSAtari911            51: "��️",  // Light drizzle
1111*1d05cddcSAtari911            53: "��️",  // Moderate drizzle
1112*1d05cddcSAtari911            55: "��️",  // Dense drizzle
1113*1d05cddcSAtari911            61: "��️",  // Slight rain
1114*1d05cddcSAtari911            63: "��️",  // Moderate rain
1115*1d05cddcSAtari911            65: "⛈️",  // Heavy rain
1116*1d05cddcSAtari911            71: "��️",  // Slight snow
1117*1d05cddcSAtari911            73: "��️",  // Moderate snow
1118*1d05cddcSAtari911            75: "❄️",  // Heavy snow
1119*1d05cddcSAtari911            77: "��️",  // Snow grains
1120*1d05cddcSAtari911            80: "��️",  // Slight rain showers
1121*1d05cddcSAtari911            81: "��️",  // Moderate rain showers
1122*1d05cddcSAtari911            82: "⛈️",  // Violent rain showers
1123*1d05cddcSAtari911            85: "��️",  // Slight snow showers
1124*1d05cddcSAtari911            86: "❄️",  // Heavy snow showers
1125*1d05cddcSAtari911            95: "⛈️",  // Thunderstorm
1126*1d05cddcSAtari911            96: "⛈️",  // Thunderstorm with slight hail
1127*1d05cddcSAtari911            99: "⛈️"   // Thunderstorm with heavy hail
1128*1d05cddcSAtari911        };
1129*1d05cddcSAtari911        return icons[code] || "��️";
1130*1d05cddcSAtari911    }
1131*1d05cddcSAtari911
1132*1d05cddcSAtari911    // Update weather immediately and every 10 minutes
1133*1d05cddcSAtari911    updateWeather();
1134*1d05cddcSAtari911    setInterval(updateWeather, 600000);
1135*1d05cddcSAtari911
1136*1d05cddcSAtari911    // CPU load history for 4-second rolling average
1137*1d05cddcSAtari911    const cpuHistory = [];
1138*1d05cddcSAtari911    const CPU_HISTORY_SIZE = 2; // 2 samples × 2 seconds = 4 seconds
1139*1d05cddcSAtari911
1140*1d05cddcSAtari911    // Store latest system stats for tooltips
1141*1d05cddcSAtari911    let latestStats = {
1142*1d05cddcSAtari911        load: {"1min": 0, "5min": 0, "15min": 0},
1143*1d05cddcSAtari911        uptime: "",
1144*1d05cddcSAtari911        memory_details: {},
1145*1d05cddcSAtari911        top_processes: []
1146*1d05cddcSAtari911    };
1147*1d05cddcSAtari911
1148*1d05cddcSAtari911    // Tooltip functions
1149*1d05cddcSAtari911    window["showTooltip_' . $calId . '"] = function(color) {
1150*1d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
1151*1d05cddcSAtari911        if (!tooltip) {
1152*1d05cddcSAtari911            console.log("Tooltip element not found for color:", color);
1153*1d05cddcSAtari911            return;
1154*1d05cddcSAtari911        }
1155*1d05cddcSAtari911
1156*1d05cddcSAtari911        console.log("Showing tooltip for:", color, "latestStats:", latestStats);
1157*1d05cddcSAtari911
1158*1d05cddcSAtari911        let content = "";
1159*1d05cddcSAtari911
1160*1d05cddcSAtari911        if (color === "green") {
1161*1d05cddcSAtari911            // Green bar: Load averages and uptime
1162*1d05cddcSAtari911            content = "<div class=\"tooltip-title\">CPU Load Average</div>";
1163*1d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
1164*1d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
1165*1d05cddcSAtari911            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
1166*1d05cddcSAtari911            if (latestStats.uptime) {
1167*1d05cddcSAtari911                content += "<div style=\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\">Uptime: " + latestStats.uptime + "</div>";
1168*1d05cddcSAtari911            }
1169*1d05cddcSAtari911            tooltip.style.borderColor = "#00cc07";
1170*1d05cddcSAtari911            tooltip.style.color = "#00cc07";
1171*1d05cddcSAtari911        } else if (color === "purple") {
1172*1d05cddcSAtari911            // Purple bar: Load averages (short-term) and top processes
1173*1d05cddcSAtari911            content = "<div class=\"tooltip-title\">CPU Load (Short-term)</div>";
1174*1d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
1175*1d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
1176*1d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
1177*1d05cddcSAtari911                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>";
1178*1d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
1179*1d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
1180*1d05cddcSAtari911                });
1181*1d05cddcSAtari911            }
1182*1d05cddcSAtari911            tooltip.style.borderColor = "#9b59b6";
1183*1d05cddcSAtari911            tooltip.style.color = "#9b59b6";
1184*1d05cddcSAtari911        } else if (color === "orange") {
1185*1d05cddcSAtari911            // Orange bar: Memory details and top processes
1186*1d05cddcSAtari911            content = "<div class=\"tooltip-title\">Memory Usage</div>";
1187*1d05cddcSAtari911            if (latestStats.memory_details && latestStats.memory_details.total) {
1188*1d05cddcSAtari911                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
1189*1d05cddcSAtari911                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
1190*1d05cddcSAtari911                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
1191*1d05cddcSAtari911                if (latestStats.memory_details.cached) {
1192*1d05cddcSAtari911                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
1193*1d05cddcSAtari911                }
1194*1d05cddcSAtari911            } else {
1195*1d05cddcSAtari911                content += "<div>Loading...</div>";
1196*1d05cddcSAtari911            }
1197*1d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
1198*1d05cddcSAtari911                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>";
1199*1d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
1200*1d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
1201*1d05cddcSAtari911                });
1202*1d05cddcSAtari911            }
1203*1d05cddcSAtari911            tooltip.style.borderColor = "#ff9800";
1204*1d05cddcSAtari911            tooltip.style.color = "#ff9800";
1205*1d05cddcSAtari911        }
1206*1d05cddcSAtari911
1207*1d05cddcSAtari911        console.log("Tooltip content:", content);
1208*1d05cddcSAtari911        tooltip.innerHTML = content;
1209*1d05cddcSAtari911        tooltip.style.display = "block";
1210*1d05cddcSAtari911
1211*1d05cddcSAtari911        // Position tooltip using fixed positioning above the bar
1212*1d05cddcSAtari911        const bar = tooltip.parentElement;
1213*1d05cddcSAtari911        const barRect = bar.getBoundingClientRect();
1214*1d05cddcSAtari911        const tooltipRect = tooltip.getBoundingClientRect();
1215*1d05cddcSAtari911
1216*1d05cddcSAtari911        // Center horizontally on the bar
1217*1d05cddcSAtari911        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
1218*1d05cddcSAtari911        // Position above the bar with 8px gap
1219*1d05cddcSAtari911        const top = barRect.top - tooltipRect.height - 8;
1220*1d05cddcSAtari911
1221*1d05cddcSAtari911        tooltip.style.left = left + "px";
1222*1d05cddcSAtari911        tooltip.style.top = top + "px";
1223*1d05cddcSAtari911    };
1224*1d05cddcSAtari911
1225*1d05cddcSAtari911    window["hideTooltip_' . $calId . '"] = function(color) {
1226*1d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
1227*1d05cddcSAtari911        if (tooltip) {
1228*1d05cddcSAtari911            tooltip.style.display = "none";
1229*1d05cddcSAtari911        }
1230*1d05cddcSAtari911    };
1231*1d05cddcSAtari911
1232*1d05cddcSAtari911    // Update CPU and memory bars every 2 seconds
1233*1d05cddcSAtari911    function updateSystemStats() {
1234*1d05cddcSAtari911        // Fetch real system stats from server
1235*1d05cddcSAtari911        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
1236*1d05cddcSAtari911            .then(response => response.json())
1237*1d05cddcSAtari911            .then(data => {
1238*1d05cddcSAtari911                console.log("System stats received:", data);
1239*1d05cddcSAtari911
1240*1d05cddcSAtari911                // Store data for tooltips
1241*1d05cddcSAtari911                latestStats = {
1242*1d05cddcSAtari911                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
1243*1d05cddcSAtari911                    uptime: data.uptime || "",
1244*1d05cddcSAtari911                    memory_details: data.memory_details || {},
1245*1d05cddcSAtari911                    top_processes: data.top_processes || []
1246*1d05cddcSAtari911                };
1247*1d05cddcSAtari911
1248*1d05cddcSAtari911                console.log("latestStats updated to:", latestStats);
1249*1d05cddcSAtari911
1250*1d05cddcSAtari911                // Update green bar (5-minute average) - updates live now!
1251*1d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
1252*1d05cddcSAtari911                if (greenBar) {
1253*1d05cddcSAtari911                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
1254*1d05cddcSAtari911                }
1255*1d05cddcSAtari911
1256*1d05cddcSAtari911                // Add current CPU to history for purple bar
1257*1d05cddcSAtari911                cpuHistory.push(data.cpu);
1258*1d05cddcSAtari911                if (cpuHistory.length > CPU_HISTORY_SIZE) {
1259*1d05cddcSAtari911                    cpuHistory.shift(); // Remove oldest
1260*1d05cddcSAtari911                }
1261*1d05cddcSAtari911
1262*1d05cddcSAtari911                // Calculate 5-second average for CPU
1263*1d05cddcSAtari911                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
1264*1d05cddcSAtari911
1265*1d05cddcSAtari911                // Update CPU bar (purple) with 5-second average
1266*1d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
1267*1d05cddcSAtari911                if (cpuBar) {
1268*1d05cddcSAtari911                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
1269*1d05cddcSAtari911                }
1270*1d05cddcSAtari911
1271*1d05cddcSAtari911                // Update memory bar (orange) with real data
1272*1d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
1273*1d05cddcSAtari911                if (memBar) {
1274*1d05cddcSAtari911                    memBar.style.width = Math.min(100, data.memory) + "%";
1275*1d05cddcSAtari911                }
1276*1d05cddcSAtari911            })
1277*1d05cddcSAtari911            .catch(error => {
1278*1d05cddcSAtari911                console.log("System stats error:", error);
1279*1d05cddcSAtari911                // Fallback to client-side estimates on error
1280*1d05cddcSAtari911                const cpuFallback = Math.random() * 100;
1281*1d05cddcSAtari911                cpuHistory.push(cpuFallback);
1282*1d05cddcSAtari911                if (cpuHistory.length > CPU_HISTORY_SIZE) {
1283*1d05cddcSAtari911                    cpuHistory.shift();
1284*1d05cddcSAtari911                }
1285*1d05cddcSAtari911                const cpuAverage = cpuHistory.reduce((sum, val) => sum + val, 0) / cpuHistory.length;
1286*1d05cddcSAtari911
1287*1d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
1288*1d05cddcSAtari911                if (greenBar) greenBar.style.width = Math.min(100, cpuFallback) + "%";
1289*1d05cddcSAtari911
1290*1d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
1291*1d05cddcSAtari911                if (cpuBar) cpuBar.style.width = Math.min(100, cpuAverage) + "%";
1292*1d05cddcSAtari911
1293*1d05cddcSAtari911                let memoryUsage = 0;
1294*1d05cddcSAtari911                if (performance.memory) {
1295*1d05cddcSAtari911                    memoryUsage = (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100;
1296*1d05cddcSAtari911                } else {
1297*1d05cddcSAtari911                    memoryUsage = Math.random() * 100;
1298*1d05cddcSAtari911                }
1299*1d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
1300*1d05cddcSAtari911                if (memBar) memBar.style.width = Math.min(100, memoryUsage) + "%";
1301*1d05cddcSAtari911            });
1302*1d05cddcSAtari911    }
1303*1d05cddcSAtari911
1304*1d05cddcSAtari911    // Update immediately and then every 2 seconds
1305*1d05cddcSAtari911    updateSystemStats();
1306*1d05cddcSAtari911    setInterval(updateSystemStats, 2000);
1307*1d05cddcSAtari911})();
1308*1d05cddcSAtari911</script>';
1309*1d05cddcSAtari911        }
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
1322*1d05cddcSAtari911            $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
1329*1d05cddcSAtari911                // 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);
1332*1d05cddcSAtari911                $isToday = $enableHighlighting && ($dateKey === $todayStr);
1333e3a9f44cSAtari911                $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow);
1334*1d05cddcSAtari911                $isPast = $dateKey < $todayStr;
133519378907SAtari911
133619378907SAtari911                foreach ($dayEvents as $event) {
1337*1d05cddcSAtari911                    // Check if this is a task and if it's completed
1338*1d05cddcSAtari911                    $isTask = !empty($event['isTask']);
1339*1d05cddcSAtari911                    $completed = !empty($event['completed']);
1340*1d05cddcSAtari911
1341*1d05cddcSAtari911                    // ALWAYS skip completed tasks UNLESS showchecked is explicitly set
1342*1d05cddcSAtari911                    if (!$showchecked && $isTask && $completed) {
1343e3a9f44cSAtari911                        continue;
1344e3a9f44cSAtari911                    }
134519378907SAtari911
1346*1d05cddcSAtari911                    // Skip past events that are NOT tasks (only show past due tasks from the past)
1347*1d05cddcSAtari911                    if ($isPast && !$isTask) {
1348*1d05cddcSAtari911                        continue;
1349*1d05cddcSAtari911                    }
1350*1d05cddcSAtari911
1351*1d05cddcSAtari911                    // Determine if task is past due (past date, is task, not completed)
1352*1d05cddcSAtari911                    $isPastDue = $isPast && $isTask && !$completed;
1353*1d05cddcSAtari911
1354e3a9f44cSAtari911                    // Line 1: Header (Title, Time, Date, Namespace)
1355e3a9f44cSAtari911                    $todayClass = $isToday ? ' eventlist-simple-today' : '';
1356e3a9f44cSAtari911                    $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : '';
1357*1d05cddcSAtari911                    $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : '';
1358*1d05cddcSAtari911                    $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
1380*1d05cddcSAtari911                    // Badge: PAST DUE, TODAY, or nothing
1381*1d05cddcSAtari911                    if ($isPastDue) {
1382*1d05cddcSAtari911                        $html .= ' <span class="eventlist-simple-pastdue-badge">PAST DUE</span>';
1383*1d05cddcSAtari911                    } 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
1432*1d05cddcSAtari911        // 1. TITLE
1433*1d05cddcSAtari911        $html .= '<div class="form-field">';
1434*1d05cddcSAtari911        $html .= '<label class="field-label">�� Title</label>';
1435*1d05cddcSAtari911        $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
1438*1d05cddcSAtari911        // 1.5 NAMESPACE SELECTOR (Searchable with fuzzy matching)
1439*1d05cddcSAtari911        $html .= '<div class="form-field">';
1440*1d05cddcSAtari911        $html .= '<label class="field-label">�� Namespace</label>';
1441*1d05cddcSAtari911
1442*1d05cddcSAtari911        // Hidden field to store actual selected namespace
1443*1d05cddcSAtari911        $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">';
1444*1d05cddcSAtari911
1445*1d05cddcSAtari911        // Searchable input
1446*1d05cddcSAtari911        $html .= '<div class="namespace-search-wrapper">';
1447*1d05cddcSAtari911        $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">';
1448*1d05cddcSAtari911        $html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="display:none;"></div>';
1449*1d05cddcSAtari911        $html .= '</div>';
1450*1d05cddcSAtari911
1451*1d05cddcSAtari911        // Store namespaces as JSON for JavaScript
1452*1d05cddcSAtari911        $allNamespaces = $this->getAllNamespaces();
1453*1d05cddcSAtari911        $html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($allNamespaces) . '</script>';
1454*1d05cddcSAtari911
1455*1d05cddcSAtari911        $html .= '</div>';
1456*1d05cddcSAtari911
1457*1d05cddcSAtari911        // 2. DESCRIPTION
1458*1d05cddcSAtari911        $html .= '<div class="form-field">';
1459*1d05cddcSAtari911        $html .= '<label class="field-label">�� Description</label>';
1460*1d05cddcSAtari911        $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="1" class="input-sleek textarea-sleek textarea-compact" placeholder="Optional details..."></textarea>';
1461*1d05cddcSAtari911        $html .= '</div>';
1462*1d05cddcSAtari911
1463*1d05cddcSAtari911        // 3. START DATE - END DATE (inline)
146419378907SAtari911        $html .= '<div class="form-row-group">';
146519378907SAtari911
1466*1d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
1467*1d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Start Date</label>';
1468*1d05cddcSAtari911        $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date input-compact">';
146919378907SAtari911        $html .= '</div>';
147019378907SAtari911
1471*1d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
1472*1d05cddcSAtari911        $html .= '<label class="field-label-compact">�� End Date</label>';
1473*1d05cddcSAtari911        $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date input-compact" placeholder="Optional">';
147419378907SAtari911        $html .= '</div>';
147519378907SAtari911
1476*1d05cddcSAtari911        $html .= '</div>'; // End row
147719378907SAtari911
1478*1d05cddcSAtari911        // 4. IS REPEATING CHECKBOX
1479*1d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
1480*1d05cddcSAtari911        $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
1486*1d05cddcSAtari911        // Recurring options (shown when checkbox is checked)
148787ac9bf3SAtari911        $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none;">';
148887ac9bf3SAtari911
1489*1d05cddcSAtari911        $html .= '<div class="form-row-group">';
1490*1d05cddcSAtari911
1491*1d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
1492*1d05cddcSAtari911        $html .= '<label class="field-label-compact">Repeat Every</label>';
1493*1d05cddcSAtari911        $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
1501*1d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
1502*1d05cddcSAtari911        $html .= '<label class="field-label-compact">Repeat Until</label>';
1503*1d05cddcSAtari911        $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date input-compact" placeholder="Optional">';
150487ac9bf3SAtari911        $html .= '</div>';
150587ac9bf3SAtari911
1506*1d05cddcSAtari911        $html .= '</div>'; // End row
1507*1d05cddcSAtari911        $html .= '</div>'; // End recurring options
150887ac9bf3SAtari911
1509*1d05cddcSAtari911        // 5. TIME (Start & End) - COLOR (inline)
1510*1d05cddcSAtari911        $html .= '<div class="form-row-group">';
1511*1d05cddcSAtari911
1512*1d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
1513*1d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Start Time</label>';
1514*1d05cddcSAtari911        $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek input-compact" onchange="updateEndTimeOptions(\'' . $calId . '\')">';
1515*1d05cddcSAtari911        $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
1531*1d05cddcSAtari911        $html .= '<div class="form-field form-field-half">';
1532*1d05cddcSAtari911        $html .= '<label class="field-label-compact">�� End Time</label>';
1533*1d05cddcSAtari911        $html .= '<select id="event-end-time-' . $calId . '" name="endTime" class="input-sleek input-compact">';
1534*1d05cddcSAtari911        $html .= '<option value="">Same as start</option>';
1535*1d05cddcSAtari911
1536*1d05cddcSAtari911        // Generate time options in 15-minute intervals
1537*1d05cddcSAtari911        for ($hour = 0; $hour < 24; $hour++) {
1538*1d05cddcSAtari911            for ($minute = 0; $minute < 60; $minute += 15) {
1539*1d05cddcSAtari911                $timeValue = sprintf('%02d:%02d', $hour, $minute);
1540*1d05cddcSAtari911                $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
1541*1d05cddcSAtari911                $ampm = $hour < 12 ? 'AM' : 'PM';
1542*1d05cddcSAtari911                $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm);
1543*1d05cddcSAtari911                $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>';
1544*1d05cddcSAtari911            }
1545*1d05cddcSAtari911        }
1546*1d05cddcSAtari911
1547*1d05cddcSAtari911        $html .= '</select>';
154819378907SAtari911        $html .= '</div>';
154919378907SAtari911
1550*1d05cddcSAtari911        $html .= '</div>'; // End row
1551*1d05cddcSAtari911
1552*1d05cddcSAtari911        // Color field (new row)
1553*1d05cddcSAtari911        $html .= '<div class="form-row-group">';
1554*1d05cddcSAtari911
1555*1d05cddcSAtari911        $html .= '<div class="form-field form-field-full">';
1556*1d05cddcSAtari911        $html .= '<label class="field-label-compact">�� Color</label>';
1557*1d05cddcSAtari911        $html .= '<div class="color-picker-wrapper">';
1558*1d05cddcSAtari911        $html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact color-select" onchange="updateCustomColorPicker(\'' . $calId . '\')">';
1559*1d05cddcSAtari911        $html .= '<option value="#3498db" style="background:#3498db;color:white">�� Blue</option>';
1560*1d05cddcSAtari911        $html .= '<option value="#2ecc71" style="background:#2ecc71;color:white">�� Green</option>';
1561*1d05cddcSAtari911        $html .= '<option value="#e74c3c" style="background:#e74c3c;color:white">�� Red</option>';
1562*1d05cddcSAtari911        $html .= '<option value="#f39c12" style="background:#f39c12;color:white">�� Orange</option>';
1563*1d05cddcSAtari911        $html .= '<option value="#9b59b6" style="background:#9b59b6;color:white">�� Purple</option>';
1564*1d05cddcSAtari911        $html .= '<option value="#e91e63" style="background:#e91e63;color:white">�� Pink</option>';
1565*1d05cddcSAtari911        $html .= '<option value="#1abc9c" style="background:#1abc9c;color:white">�� Teal</option>';
1566*1d05cddcSAtari911        $html .= '<option value="custom">�� Custom...</option>';
1567*1d05cddcSAtari911        $html .= '</select>';
1568*1d05cddcSAtari911        $html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input color-picker-compact" value="#3498db" onchange="updateColorFromPicker(\'' . $calId . '\')">';
1569*1d05cddcSAtari911        $html .= '</div>';
157019378907SAtari911        $html .= '</div>';
157119378907SAtari911
1572*1d05cddcSAtari911        $html .= '</div>'; // End row
1573*1d05cddcSAtari911
1574*1d05cddcSAtari911        // Task checkbox
1575*1d05cddcSAtari911        $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
1576*1d05cddcSAtari911        $html .= '<label class="checkbox-label checkbox-label-compact">';
1577*1d05cddcSAtari911        $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">';
1578*1d05cddcSAtari911        $html .= '<span>�� This is a task (can be checked off)</span>';
1579*1d05cddcSAtari911        $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    }
1879*1d05cddcSAtari911
1880*1d05cddcSAtari911    private function getAllNamespaces() {
1881*1d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
1882*1d05cddcSAtari911        $namespaces = [];
1883*1d05cddcSAtari911
1884*1d05cddcSAtari911        // Scan for namespaces that have calendar data
1885*1d05cddcSAtari911        $this->scanForCalendarNamespaces($dataDir, '', $namespaces);
1886*1d05cddcSAtari911
1887*1d05cddcSAtari911        // Sort alphabetically
1888*1d05cddcSAtari911        sort($namespaces);
1889*1d05cddcSAtari911
1890*1d05cddcSAtari911        return $namespaces;
1891*1d05cddcSAtari911    }
1892*1d05cddcSAtari911
1893*1d05cddcSAtari911    private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) {
1894*1d05cddcSAtari911        if (!is_dir($dir)) return;
1895*1d05cddcSAtari911
1896*1d05cddcSAtari911        $items = scandir($dir);
1897*1d05cddcSAtari911        foreach ($items as $item) {
1898*1d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
1899*1d05cddcSAtari911
1900*1d05cddcSAtari911            $path = $dir . $item;
1901*1d05cddcSAtari911            if (is_dir($path)) {
1902*1d05cddcSAtari911                // Check if this directory has a calendar subdirectory with data
1903*1d05cddcSAtari911                $calendarDir = $path . '/calendar/';
1904*1d05cddcSAtari911                if (is_dir($calendarDir)) {
1905*1d05cddcSAtari911                    // Check if there are any JSON files in the calendar directory
1906*1d05cddcSAtari911                    $jsonFiles = glob($calendarDir . '*.json');
1907*1d05cddcSAtari911                    if (!empty($jsonFiles)) {
1908*1d05cddcSAtari911                        // This namespace has calendar data
1909*1d05cddcSAtari911                        $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
1910*1d05cddcSAtari911                        $namespaces[] = $namespace;
1911*1d05cddcSAtari911                    }
1912*1d05cddcSAtari911                }
1913*1d05cddcSAtari911
1914*1d05cddcSAtari911                // Recurse into subdirectories
1915*1d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
1916*1d05cddcSAtari911                $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces);
1917*1d05cddcSAtari911            }
1918*1d05cddcSAtari911        }
1919*1d05cddcSAtari911    }
1920*1d05cddcSAtari911
1921*1d05cddcSAtari911    /**
1922*1d05cddcSAtari911     * Render new sidebar widget - Week at a glance itinerary (200px wide)
1923*1d05cddcSAtari911     */
1924*1d05cddcSAtari911    private function renderSidebarWidget($events, $namespace, $calId) {
1925*1d05cddcSAtari911        if (empty($events)) {
1926*1d05cddcSAtari911            return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No events this week</div>';
1927*1d05cddcSAtari911        }
1928*1d05cddcSAtari911
1929*1d05cddcSAtari911        // Get important namespaces from config
1930*1d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
1931*1d05cddcSAtari911        $importantNsList = ['important']; // default
1932*1d05cddcSAtari911        if (file_exists($configFile)) {
1933*1d05cddcSAtari911            $config = include $configFile;
1934*1d05cddcSAtari911            if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
1935*1d05cddcSAtari911                $importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
1936*1d05cddcSAtari911            }
1937*1d05cddcSAtari911        }
1938*1d05cddcSAtari911
1939*1d05cddcSAtari911        // Calculate date ranges
1940*1d05cddcSAtari911        $todayStr = date('Y-m-d');
1941*1d05cddcSAtari911        $tomorrowStr = date('Y-m-d', strtotime('+1 day'));
1942*1d05cddcSAtari911        $weekStart = date('Y-m-d', strtotime('monday this week'));
1943*1d05cddcSAtari911        $weekEnd = date('Y-m-d', strtotime('sunday this week'));
1944*1d05cddcSAtari911
1945*1d05cddcSAtari911        // Group events by category
1946*1d05cddcSAtari911        $todayEvents = [];
1947*1d05cddcSAtari911        $tomorrowEvents = [];
1948*1d05cddcSAtari911        $importantEvents = [];
1949*1d05cddcSAtari911        $weekEvents = []; // For week grid
1950*1d05cddcSAtari911
1951*1d05cddcSAtari911        // Process all events
1952*1d05cddcSAtari911        foreach ($events as $dateKey => $dayEvents) {
1953*1d05cddcSAtari911            // Skip events before this week
1954*1d05cddcSAtari911            if ($dateKey < $weekStart) continue;
1955*1d05cddcSAtari911
1956*1d05cddcSAtari911            // Initialize week grid day if in current week
1957*1d05cddcSAtari911            if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
1958*1d05cddcSAtari911                if (!isset($weekEvents[$dateKey])) {
1959*1d05cddcSAtari911                    $weekEvents[$dateKey] = [];
1960*1d05cddcSAtari911                }
1961*1d05cddcSAtari911            }
1962*1d05cddcSAtari911
1963*1d05cddcSAtari911            foreach ($dayEvents as $event) {
1964*1d05cddcSAtari911                // Add to week grid if in week range
1965*1d05cddcSAtari911                if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
1966*1d05cddcSAtari911                    // Pre-render DokuWiki syntax to HTML for JavaScript display
1967*1d05cddcSAtari911                    $eventWithHtml = $event;
1968*1d05cddcSAtari911                    if (isset($event['title'])) {
1969*1d05cddcSAtari911                        $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']);
1970*1d05cddcSAtari911                    }
1971*1d05cddcSAtari911                    if (isset($event['description'])) {
1972*1d05cddcSAtari911                        $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']);
1973*1d05cddcSAtari911                    }
1974*1d05cddcSAtari911                    $weekEvents[$dateKey][] = $eventWithHtml;
1975*1d05cddcSAtari911                }
1976*1d05cddcSAtari911
1977*1d05cddcSAtari911                // Categorize for detailed sections
1978*1d05cddcSAtari911                if ($dateKey === $todayStr) {
1979*1d05cddcSAtari911                    $todayEvents[] = array_merge($event, ['date' => $dateKey]);
1980*1d05cddcSAtari911                } elseif ($dateKey === $tomorrowStr) {
1981*1d05cddcSAtari911                    $tomorrowEvents[] = array_merge($event, ['date' => $dateKey]);
1982*1d05cddcSAtari911                } else {
1983*1d05cddcSAtari911                    // Check if this is an important namespace
1984*1d05cddcSAtari911                    $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
1985*1d05cddcSAtari911                    $isImportant = false;
1986*1d05cddcSAtari911                    foreach ($importantNsList as $impNs) {
1987*1d05cddcSAtari911                        if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
1988*1d05cddcSAtari911                            $isImportant = true;
1989*1d05cddcSAtari911                            break;
1990*1d05cddcSAtari911                        }
1991*1d05cddcSAtari911                    }
1992*1d05cddcSAtari911
1993*1d05cddcSAtari911                    // Important events: this week but not today/tomorrow
1994*1d05cddcSAtari911                    if ($isImportant && $dateKey >= $weekStart && $dateKey <= $weekEnd) {
1995*1d05cddcSAtari911                        $importantEvents[] = array_merge($event, ['date' => $dateKey]);
1996*1d05cddcSAtari911                    }
1997*1d05cddcSAtari911                }
1998*1d05cddcSAtari911            }
1999*1d05cddcSAtari911        }
2000*1d05cddcSAtari911
2001*1d05cddcSAtari911        // Start building HTML - Dynamic width with default font
2002*1d05cddcSAtari911        $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);">';
2003*1d05cddcSAtari911
2004*1d05cddcSAtari911        // Sanitize calId for use in JavaScript variable names (remove dashes)
2005*1d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
2006*1d05cddcSAtari911
2007*1d05cddcSAtari911        // CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it
2008*1d05cddcSAtari911        $html .= '<script>
2009*1d05cddcSAtari911(function() {
2010*1d05cddcSAtari911    // Shared state for system stats and tooltips
2011*1d05cddcSAtari911    const sharedState_' . $jsCalId . ' = {
2012*1d05cddcSAtari911        latestStats: {
2013*1d05cddcSAtari911            load: {"1min": 0, "5min": 0, "15min": 0},
2014*1d05cddcSAtari911            uptime: "",
2015*1d05cddcSAtari911            memory_details: {},
2016*1d05cddcSAtari911            top_processes: []
2017*1d05cddcSAtari911        },
2018*1d05cddcSAtari911        cpuHistory: [],
2019*1d05cddcSAtari911        CPU_HISTORY_SIZE: 2
2020*1d05cddcSAtari911    };
2021*1d05cddcSAtari911
2022*1d05cddcSAtari911    // Tooltip functions - MUST be defined before HTML uses them
2023*1d05cddcSAtari911    window["showTooltip_' . $jsCalId . '"] = function(color) {
2024*1d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
2025*1d05cddcSAtari911        if (!tooltip) {
2026*1d05cddcSAtari911            console.log("Tooltip element not found for color:", color);
2027*1d05cddcSAtari911            return;
2028*1d05cddcSAtari911        }
2029*1d05cddcSAtari911
2030*1d05cddcSAtari911        const latestStats = sharedState_' . $jsCalId . '.latestStats;
2031*1d05cddcSAtari911        let content = "";
2032*1d05cddcSAtari911
2033*1d05cddcSAtari911        if (color === "green") {
2034*1d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">CPU Load Average</div>";
2035*1d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
2036*1d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
2037*1d05cddcSAtari911            content += "<div>15 min: " + (latestStats.load["15min"] || "N/A") + "</div>";
2038*1d05cddcSAtari911            if (latestStats.uptime) {
2039*1d05cddcSAtari911                content += "<div style=\\"margin-top:3px; padding-top:2px; border-top:1px solid rgba(0,204,7,0.3);\\">Uptime: " + latestStats.uptime + "</div>";
2040*1d05cddcSAtari911            }
2041*1d05cddcSAtari911            tooltip.style.borderColor = "#00cc07";
2042*1d05cddcSAtari911            tooltip.style.color = "#00cc07";
2043*1d05cddcSAtari911        } else if (color === "purple") {
2044*1d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">CPU Load (Short-term)</div>";
2045*1d05cddcSAtari911            content += "<div>1 min: " + (latestStats.load["1min"] || "N/A") + "</div>";
2046*1d05cddcSAtari911            content += "<div>5 min: " + (latestStats.load["5min"] || "N/A") + "</div>";
2047*1d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
2048*1d05cddcSAtari911                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>";
2049*1d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
2050*1d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
2051*1d05cddcSAtari911                });
2052*1d05cddcSAtari911            }
2053*1d05cddcSAtari911            tooltip.style.borderColor = "#9b59b6";
2054*1d05cddcSAtari911            tooltip.style.color = "#9b59b6";
2055*1d05cddcSAtari911        } else if (color === "orange") {
2056*1d05cddcSAtari911            content = "<div class=\\"tooltip-title\\">Memory Usage</div>";
2057*1d05cddcSAtari911            if (latestStats.memory_details && latestStats.memory_details.total) {
2058*1d05cddcSAtari911                content += "<div>Total: " + latestStats.memory_details.total + "</div>";
2059*1d05cddcSAtari911                content += "<div>Used: " + latestStats.memory_details.used + "</div>";
2060*1d05cddcSAtari911                content += "<div>Available: " + latestStats.memory_details.available + "</div>";
2061*1d05cddcSAtari911                if (latestStats.memory_details.cached) {
2062*1d05cddcSAtari911                    content += "<div>Cached: " + latestStats.memory_details.cached + "</div>";
2063*1d05cddcSAtari911                }
2064*1d05cddcSAtari911            } else {
2065*1d05cddcSAtari911                content += "<div>Loading...</div>";
2066*1d05cddcSAtari911            }
2067*1d05cddcSAtari911            if (latestStats.top_processes && latestStats.top_processes.length > 0) {
2068*1d05cddcSAtari911                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>";
2069*1d05cddcSAtari911                latestStats.top_processes.slice(0, 5).forEach(proc => {
2070*1d05cddcSAtari911                    content += "<div>" + proc.cpu + " " + proc.command + "</div>";
2071*1d05cddcSAtari911                });
2072*1d05cddcSAtari911            }
2073*1d05cddcSAtari911            tooltip.style.borderColor = "#ff9800";
2074*1d05cddcSAtari911            tooltip.style.color = "#ff9800";
2075*1d05cddcSAtari911        }
2076*1d05cddcSAtari911
2077*1d05cddcSAtari911        tooltip.innerHTML = content;
2078*1d05cddcSAtari911        tooltip.style.display = "block";
2079*1d05cddcSAtari911
2080*1d05cddcSAtari911        const bar = tooltip.parentElement;
2081*1d05cddcSAtari911        const barRect = bar.getBoundingClientRect();
2082*1d05cddcSAtari911        const tooltipRect = tooltip.getBoundingClientRect();
2083*1d05cddcSAtari911
2084*1d05cddcSAtari911        const left = barRect.left + (barRect.width / 2) - (tooltipRect.width / 2);
2085*1d05cddcSAtari911        const top = barRect.top - tooltipRect.height - 8;
2086*1d05cddcSAtari911
2087*1d05cddcSAtari911        tooltip.style.left = left + "px";
2088*1d05cddcSAtari911        tooltip.style.top = top + "px";
2089*1d05cddcSAtari911    };
2090*1d05cddcSAtari911
2091*1d05cddcSAtari911    window["hideTooltip_' . $jsCalId . '"] = function(color) {
2092*1d05cddcSAtari911        const tooltip = document.getElementById("tooltip-" + color + "-' . $calId . '");
2093*1d05cddcSAtari911        if (tooltip) {
2094*1d05cddcSAtari911            tooltip.style.display = "none";
2095*1d05cddcSAtari911        }
2096*1d05cddcSAtari911    };
2097*1d05cddcSAtari911
2098*1d05cddcSAtari911    // Update clock every second
2099*1d05cddcSAtari911    function updateClock() {
2100*1d05cddcSAtari911        const now = new Date();
2101*1d05cddcSAtari911        let hours = now.getHours();
2102*1d05cddcSAtari911        const minutes = String(now.getMinutes()).padStart(2, "0");
2103*1d05cddcSAtari911        const seconds = String(now.getSeconds()).padStart(2, "0");
2104*1d05cddcSAtari911        const ampm = hours >= 12 ? "PM" : "AM";
2105*1d05cddcSAtari911        hours = hours % 12 || 12;
2106*1d05cddcSAtari911        const timeStr = hours + ":" + minutes + ":" + seconds + " " + ampm;
2107*1d05cddcSAtari911        const clockEl = document.getElementById("clock-' . $calId . '");
2108*1d05cddcSAtari911        if (clockEl) clockEl.textContent = timeStr;
2109*1d05cddcSAtari911    }
2110*1d05cddcSAtari911    setInterval(updateClock, 1000);
2111*1d05cddcSAtari911
2112*1d05cddcSAtari911    // Weather update function
2113*1d05cddcSAtari911    function updateWeather() {
2114*1d05cddcSAtari911        if ("geolocation" in navigator) {
2115*1d05cddcSAtari911            navigator.geolocation.getCurrentPosition(function(position) {
2116*1d05cddcSAtari911                const lat = position.coords.latitude;
2117*1d05cddcSAtari911                const lon = position.coords.longitude;
2118*1d05cddcSAtari911
2119*1d05cddcSAtari911                fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&temperature_unit=fahrenheit`)
2120*1d05cddcSAtari911                    .then(response => response.json())
2121*1d05cddcSAtari911                    .then(data => {
2122*1d05cddcSAtari911                        if (data.current_weather) {
2123*1d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
2124*1d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
2125*1d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
2126*1d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
2127*1d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
2128*1d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
2129*1d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
2130*1d05cddcSAtari911                        }
2131*1d05cddcSAtari911                    })
2132*1d05cddcSAtari911                    .catch(error => console.log("Weather fetch error:", error));
2133*1d05cddcSAtari911            }, function(error) {
2134*1d05cddcSAtari911                // If geolocation fails, use default location (Irvine, CA)
2135*1d05cddcSAtari911                fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265&current_weather=true&temperature_unit=fahrenheit")
2136*1d05cddcSAtari911                    .then(response => response.json())
2137*1d05cddcSAtari911                    .then(data => {
2138*1d05cddcSAtari911                        if (data.current_weather) {
2139*1d05cddcSAtari911                            const temp = Math.round(data.current_weather.temperature);
2140*1d05cddcSAtari911                            const weatherCode = data.current_weather.weathercode;
2141*1d05cddcSAtari911                            const icon = getWeatherIcon(weatherCode);
2142*1d05cddcSAtari911                            const iconEl = document.getElementById("weather-icon-' . $calId . '");
2143*1d05cddcSAtari911                            const tempEl = document.getElementById("weather-temp-' . $calId . '");
2144*1d05cddcSAtari911                            if (iconEl) iconEl.textContent = icon;
2145*1d05cddcSAtari911                            if (tempEl) tempEl.innerHTML = temp + "&deg;";
2146*1d05cddcSAtari911                        }
2147*1d05cddcSAtari911                    })
2148*1d05cddcSAtari911                    .catch(err => console.log("Weather error:", err));
2149*1d05cddcSAtari911            });
2150*1d05cddcSAtari911        } else {
2151*1d05cddcSAtari911            // No geolocation, use default (Irvine, CA)
2152*1d05cddcSAtari911            fetch("https://api.open-meteo.com/v1/forecast?latitude=33.6846&longitude=-117.8265&current_weather=true&temperature_unit=fahrenheit")
2153*1d05cddcSAtari911                .then(response => response.json())
2154*1d05cddcSAtari911                .then(data => {
2155*1d05cddcSAtari911                    if (data.current_weather) {
2156*1d05cddcSAtari911                        const temp = Math.round(data.current_weather.temperature);
2157*1d05cddcSAtari911                        const weatherCode = data.current_weather.weathercode;
2158*1d05cddcSAtari911                        const icon = getWeatherIcon(weatherCode);
2159*1d05cddcSAtari911                        const iconEl = document.getElementById("weather-icon-' . $calId . '");
2160*1d05cddcSAtari911                        const tempEl = document.getElementById("weather-temp-' . $calId . '");
2161*1d05cddcSAtari911                        if (iconEl) iconEl.textContent = icon;
2162*1d05cddcSAtari911                        if (tempEl) tempEl.innerHTML = temp + "&deg;";
2163*1d05cddcSAtari911                    }
2164*1d05cddcSAtari911                })
2165*1d05cddcSAtari911                .catch(err => console.log("Weather error:", err));
2166*1d05cddcSAtari911        }
2167*1d05cddcSAtari911    }
2168*1d05cddcSAtari911
2169*1d05cddcSAtari911    function getWeatherIcon(code) {
2170*1d05cddcSAtari911        const icons = {
2171*1d05cddcSAtari911            0: "☀️", 1: "��️", 2: "⛅", 3: "☁️",
2172*1d05cddcSAtari911            45: "��️", 48: "��️", 51: "��️", 53: "��️", 55: "��️",
2173*1d05cddcSAtari911            61: "��️", 63: "��️", 65: "⛈️", 71: "��️", 73: "��️",
2174*1d05cddcSAtari911            75: "❄️", 77: "��️", 80: "��️", 81: "��️", 82: "⛈️",
2175*1d05cddcSAtari911            85: "��️", 86: "❄️", 95: "⛈️", 96: "⛈️", 99: "⛈️"
2176*1d05cddcSAtari911        };
2177*1d05cddcSAtari911        return icons[code] || "��️";
2178*1d05cddcSAtari911    }
2179*1d05cddcSAtari911
2180*1d05cddcSAtari911    // Update weather immediately and every 10 minutes
2181*1d05cddcSAtari911    updateWeather();
2182*1d05cddcSAtari911    setInterval(updateWeather, 600000);
2183*1d05cddcSAtari911
2184*1d05cddcSAtari911    // Update system stats and tooltips data
2185*1d05cddcSAtari911    function updateSystemStats() {
2186*1d05cddcSAtari911        fetch("' . DOKU_BASE . 'lib/plugins/calendar/get_system_stats.php")
2187*1d05cddcSAtari911            .then(response => response.json())
2188*1d05cddcSAtari911            .then(data => {
2189*1d05cddcSAtari911                sharedState_' . $jsCalId . '.latestStats = {
2190*1d05cddcSAtari911                    load: data.load || {"1min": 0, "5min": 0, "15min": 0},
2191*1d05cddcSAtari911                    uptime: data.uptime || "",
2192*1d05cddcSAtari911                    memory_details: data.memory_details || {},
2193*1d05cddcSAtari911                    top_processes: data.top_processes || []
2194*1d05cddcSAtari911                };
2195*1d05cddcSAtari911
2196*1d05cddcSAtari911                const greenBar = document.getElementById("cpu-5min-' . $calId . '");
2197*1d05cddcSAtari911                if (greenBar) {
2198*1d05cddcSAtari911                    greenBar.style.width = Math.min(100, data.cpu_5min) + "%";
2199*1d05cddcSAtari911                }
2200*1d05cddcSAtari911
2201*1d05cddcSAtari911                sharedState_' . $jsCalId . '.cpuHistory.push(data.cpu);
2202*1d05cddcSAtari911                if (sharedState_' . $jsCalId . '.cpuHistory.length > sharedState_' . $jsCalId . '.CPU_HISTORY_SIZE) {
2203*1d05cddcSAtari911                    sharedState_' . $jsCalId . '.cpuHistory.shift();
2204*1d05cddcSAtari911                }
2205*1d05cddcSAtari911
2206*1d05cddcSAtari911                const cpuAverage = sharedState_' . $jsCalId . '.cpuHistory.reduce((sum, val) => sum + val, 0) / sharedState_' . $jsCalId . '.cpuHistory.length;
2207*1d05cddcSAtari911
2208*1d05cddcSAtari911                const cpuBar = document.getElementById("cpu-realtime-' . $calId . '");
2209*1d05cddcSAtari911                if (cpuBar) {
2210*1d05cddcSAtari911                    cpuBar.style.width = Math.min(100, cpuAverage) + "%";
2211*1d05cddcSAtari911                }
2212*1d05cddcSAtari911
2213*1d05cddcSAtari911                const memBar = document.getElementById("mem-realtime-' . $calId . '");
2214*1d05cddcSAtari911                if (memBar) {
2215*1d05cddcSAtari911                    memBar.style.width = Math.min(100, data.memory) + "%";
2216*1d05cddcSAtari911                }
2217*1d05cddcSAtari911            })
2218*1d05cddcSAtari911            .catch(error => {
2219*1d05cddcSAtari911                console.log("System stats error:", error);
2220*1d05cddcSAtari911            });
2221*1d05cddcSAtari911    }
2222*1d05cddcSAtari911
2223*1d05cddcSAtari911    updateSystemStats();
2224*1d05cddcSAtari911    setInterval(updateSystemStats, 2000);
2225*1d05cddcSAtari911})();
2226*1d05cddcSAtari911</script>';
2227*1d05cddcSAtari911
2228*1d05cddcSAtari911        // NOW add the header HTML (after JavaScript is defined)
2229*1d05cddcSAtari911        $todayDate = new DateTime();
2230*1d05cddcSAtari911        $displayDate = $todayDate->format('D, M j, Y');
2231*1d05cddcSAtari911        $currentTime = $todayDate->format('g:i:s A');
2232*1d05cddcSAtari911
2233*1d05cddcSAtari911        $html .= '<div class="eventlist-today-header">';
2234*1d05cddcSAtari911        $html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span>';
2235*1d05cddcSAtari911        $html .= '<div class="eventlist-bottom-info">';
2236*1d05cddcSAtari911        $html .= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '">--°</span></span>';
2237*1d05cddcSAtari911        $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>';
2238*1d05cddcSAtari911        $html .= '</div>';
2239*1d05cddcSAtari911
2240*1d05cddcSAtari911        // Three CPU/Memory bars (all update live)
2241*1d05cddcSAtari911        $html .= '<div class="eventlist-stats-container">';
2242*1d05cddcSAtari911
2243*1d05cddcSAtari911        // 5-minute load average (green, updates every 2 seconds)
2244*1d05cddcSAtari911        $html .= '<div class="eventlist-cpu-bar" onmouseover="showTooltip_' . $jsCalId . '(\'green\')" onmouseout="hideTooltip_' . $jsCalId . '(\'green\')">';
2245*1d05cddcSAtari911        $html .= '<div class="eventlist-cpu-fill" id="cpu-5min-' . $calId . '" style="width: 0%;"></div>';
2246*1d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-green-' . $calId . '" style="display:none;"></div>';
2247*1d05cddcSAtari911        $html .= '</div>';
2248*1d05cddcSAtari911
2249*1d05cddcSAtari911        // Real-time CPU (purple, updates with 5-sec average)
2250*1d05cddcSAtari911        $html .= '<div class="eventlist-cpu-bar eventlist-cpu-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'purple\')" onmouseout="hideTooltip_' . $jsCalId . '(\'purple\')">';
2251*1d05cddcSAtari911        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-purple" id="cpu-realtime-' . $calId . '" style="width: 0%;"></div>';
2252*1d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-purple-' . $calId . '" style="display:none;"></div>';
2253*1d05cddcSAtari911        $html .= '</div>';
2254*1d05cddcSAtari911
2255*1d05cddcSAtari911        // Real-time Memory (orange, updates)
2256*1d05cddcSAtari911        $html .= '<div class="eventlist-cpu-bar eventlist-mem-realtime" onmouseover="showTooltip_' . $jsCalId . '(\'orange\')" onmouseout="hideTooltip_' . $jsCalId . '(\'orange\')">';
2257*1d05cddcSAtari911        $html .= '<div class="eventlist-cpu-fill eventlist-cpu-fill-orange" id="mem-realtime-' . $calId . '" style="width: 0%;"></div>';
2258*1d05cddcSAtari911        $html .= '<div class="system-tooltip" id="tooltip-orange-' . $calId . '" style="display:none;"></div>';
2259*1d05cddcSAtari911        $html .= '</div>';
2260*1d05cddcSAtari911
2261*1d05cddcSAtari911        $html .= '</div>';
2262*1d05cddcSAtari911        $html .= '</div>';
2263*1d05cddcSAtari911
2264*1d05cddcSAtari911        // Ultra-thin orange "Add Event" bar between header and week grid (6px max height)
2265*1d05cddcSAtari911        $html .= '<div style="background:#ff9800; padding:0; height:6px; line-height:6px; text-align:center; cursor:pointer; border-top:1px solid rgba(255, 152, 0, 0.3); border-bottom:1px solid rgba(255, 152, 0, 0.3); box-shadow:0 0 8px rgba(255, 152, 0, 0.4); transition:all 0.2s; overflow:hidden;" onclick="window.location.href=\'?do=admin&page=calendar&tab=manage\'" onmouseover="this.style.background=\'#ff7700\'; this.style.boxShadow=\'0 0 12px rgba(255, 152, 0, 0.6)\';" onmouseout="this.style.background=\'#ff9800\'; this.style.boxShadow=\'0 0 8px rgba(255, 152, 0, 0.4)\';">';
2266*1d05cddcSAtari911        $html .= '<span style="color:#000; font-size:7px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; text-shadow:0 0 2px rgba(255, 255, 255, 0.3); vertical-align:middle;">+ ADD EVENT</span>';
2267*1d05cddcSAtari911        $html .= '</div>';
2268*1d05cddcSAtari911
2269*1d05cddcSAtari911        // Week grid (7 cells)
2270*1d05cddcSAtari911        $html .= $this->renderWeekGrid($weekEvents, $weekStart);
2271*1d05cddcSAtari911
2272*1d05cddcSAtari911        // Today section (orange)
2273*1d05cddcSAtari911        if (!empty($todayEvents)) {
2274*1d05cddcSAtari911            $html .= $this->renderSidebarSection('Today', $todayEvents, '#ff9800', $calId);
2275*1d05cddcSAtari911        }
2276*1d05cddcSAtari911
2277*1d05cddcSAtari911        // Tomorrow section (green)
2278*1d05cddcSAtari911        if (!empty($tomorrowEvents)) {
2279*1d05cddcSAtari911            $html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, '#4caf50', $calId);
2280*1d05cddcSAtari911        }
2281*1d05cddcSAtari911
2282*1d05cddcSAtari911        // Important events section (purple)
2283*1d05cddcSAtari911        if (!empty($importantEvents)) {
2284*1d05cddcSAtari911            $html .= $this->renderSidebarSection('Important Events', $importantEvents, '#9b59b6', $calId);
2285*1d05cddcSAtari911        }
2286*1d05cddcSAtari911
2287*1d05cddcSAtari911        $html .= '</div>';
2288*1d05cddcSAtari911
2289*1d05cddcSAtari911        return $html;
2290*1d05cddcSAtari911    }
2291*1d05cddcSAtari911
2292*1d05cddcSAtari911    /**
2293*1d05cddcSAtari911     * Render compact week grid (7 cells with event bars) - Matrix themed with clickable days
2294*1d05cddcSAtari911     */
2295*1d05cddcSAtari911    private function renderWeekGrid($weekEvents, $weekStart) {
2296*1d05cddcSAtari911        // Generate unique ID for this calendar instance - sanitize for JavaScript
2297*1d05cddcSAtari911        $calId = 'cal_' . substr(md5($weekStart . microtime()), 0, 8);
2298*1d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);  // Sanitize for JS variable names
2299*1d05cddcSAtari911
2300*1d05cddcSAtari911        $html = '<div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:#1a3d1a; border-bottom:2px solid #00cc07;">';
2301*1d05cddcSAtari911
2302*1d05cddcSAtari911        $dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
2303*1d05cddcSAtari911        $today = date('Y-m-d');
2304*1d05cddcSAtari911
2305*1d05cddcSAtari911        for ($i = 0; $i < 7; $i++) {
2306*1d05cddcSAtari911            $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days'));
2307*1d05cddcSAtari911            $dayNum = date('j', strtotime($date));
2308*1d05cddcSAtari911            $isToday = $date === $today;
2309*1d05cddcSAtari911
2310*1d05cddcSAtari911            $events = isset($weekEvents[$date]) ? $weekEvents[$date] : [];
2311*1d05cddcSAtari911            $eventCount = count($events);
2312*1d05cddcSAtari911
2313*1d05cddcSAtari911            $bgColor = $isToday ? '#2a4d2a' : '#242424';
2314*1d05cddcSAtari911            $textColor = $isToday ? '#00ff00' : '#00cc07';
2315*1d05cddcSAtari911            $fontWeight = $isToday ? '700' : '500';
2316*1d05cddcSAtari911            $textShadow = $isToday ? 'text-shadow:0 0 6px rgba(0, 255, 0, 0.6);' : 'text-shadow:0 0 4px rgba(0, 204, 7, 0.4);';
2317*1d05cddcSAtari911
2318*1d05cddcSAtari911            $hasEvents = $eventCount > 0;
2319*1d05cddcSAtari911            $clickableStyle = $hasEvents ? 'cursor:pointer;' : '';
2320*1d05cddcSAtari911            $clickHandler = $hasEvents ? ' onclick="showDayEvents_' . $jsCalId . '(\'' . $date . '\')"' : '';
2321*1d05cddcSAtari911
2322*1d05cddcSAtari911            $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 . '>';
2323*1d05cddcSAtari911
2324*1d05cddcSAtari911            // Day letter
2325*1d05cddcSAtari911            $html .= '<div style="font-size:9px; color:#00cc07; font-weight:500; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNames[$i] . '</div>';
2326*1d05cddcSAtari911
2327*1d05cddcSAtari911            // Day number
2328*1d05cddcSAtari911            $html .= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:2px 0; font-family:system-ui, sans-serif; ' . $textShadow . '">' . $dayNum . '</div>';
2329*1d05cddcSAtari911
2330*1d05cddcSAtari911            // Event bars (max 3 visible) with glow effect
2331*1d05cddcSAtari911            if ($eventCount > 0) {
2332*1d05cddcSAtari911                $showCount = min($eventCount, 3);
2333*1d05cddcSAtari911                for ($j = 0; $j < $showCount; $j++) {
2334*1d05cddcSAtari911                    $event = $events[$j];
2335*1d05cddcSAtari911                    $color = isset($event['color']) ? $event['color'] : '#00cc07';
2336*1d05cddcSAtari911                    $html .= '<div style="height:2px; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:0 0 3px ' . htmlspecialchars($color) . ';"></div>';
2337*1d05cddcSAtari911                }
2338*1d05cddcSAtari911
2339*1d05cddcSAtari911                // Show "+N more" if more than 3
2340*1d05cddcSAtari911                if ($eventCount > 3) {
2341*1d05cddcSAtari911                    $html .= '<div style="font-size:7px; color:#00cc07; margin-top:1px; font-family:system-ui, sans-serif;">+' . ($eventCount - 3) . '</div>';
2342*1d05cddcSAtari911                }
2343*1d05cddcSAtari911            }
2344*1d05cddcSAtari911
2345*1d05cddcSAtari911            $html .= '</div>';
2346*1d05cddcSAtari911        }
2347*1d05cddcSAtari911
2348*1d05cddcSAtari911        $html .= '</div>';
2349*1d05cddcSAtari911
2350*1d05cddcSAtari911        // Add container for selected day events display (with unique ID)
2351*1d05cddcSAtari911        $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);">';
2352*1d05cddcSAtari911        $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;">';
2353*1d05cddcSAtari911        $html .= '<span id="selected-day-title-' . $calId . '"></span>';
2354*1d05cddcSAtari911        $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>';
2355*1d05cddcSAtari911        $html .= '</div>';
2356*1d05cddcSAtari911        $html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:rgba(36, 36, 36, 0.5);"></div>';
2357*1d05cddcSAtari911        $html .= '</div>';
2358*1d05cddcSAtari911
2359*1d05cddcSAtari911        // Add JavaScript for day selection with event data
2360*1d05cddcSAtari911        $html .= '<script>';
2361*1d05cddcSAtari911        // Sanitize calId for JavaScript variable names
2362*1d05cddcSAtari911        $jsCalId = str_replace('-', '_', $calId);
2363*1d05cddcSAtari911        $html .= 'window.weekEventsData_' . $jsCalId . ' = ' . json_encode($weekEvents) . ';';
2364*1d05cddcSAtari911        $html .= '
2365*1d05cddcSAtari911        window.showDayEvents_' . $jsCalId . ' = function(dateKey) {
2366*1d05cddcSAtari911            const eventsData = window.weekEventsData_' . $jsCalId . ';
2367*1d05cddcSAtari911            const container = document.getElementById("selected-day-events-' . $calId . '");
2368*1d05cddcSAtari911            const title = document.getElementById("selected-day-title-' . $calId . '");
2369*1d05cddcSAtari911            const content = document.getElementById("selected-day-content-' . $calId . '");
2370*1d05cddcSAtari911
2371*1d05cddcSAtari911            if (!eventsData[dateKey] || eventsData[dateKey].length === 0) return;
2372*1d05cddcSAtari911
2373*1d05cddcSAtari911            // Format date for display
2374*1d05cddcSAtari911            const dateObj = new Date(dateKey + "T00:00:00");
2375*1d05cddcSAtari911            const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" });
2376*1d05cddcSAtari911            const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" });
2377*1d05cddcSAtari911            title.textContent = dayName + ", " + monthDay;
2378*1d05cddcSAtari911
2379*1d05cddcSAtari911            // Clear content
2380*1d05cddcSAtari911            content.innerHTML = "";
2381*1d05cddcSAtari911
2382*1d05cddcSAtari911            // Sort events by time (events with time first, then all-day events)
2383*1d05cddcSAtari911            const sortedEvents = [...eventsData[dateKey]].sort((a, b) => {
2384*1d05cddcSAtari911                // Events without time go to the end
2385*1d05cddcSAtari911                if (!a.time && !b.time) return 0;
2386*1d05cddcSAtari911                if (!a.time) return 1;
2387*1d05cddcSAtari911                if (!b.time) return -1;
2388*1d05cddcSAtari911
2389*1d05cddcSAtari911                // Compare times (format: "HH:MM")
2390*1d05cddcSAtari911                const timeA = a.time.split(":").map(Number);
2391*1d05cddcSAtari911                const timeB = b.time.split(":").map(Number);
2392*1d05cddcSAtari911                const minutesA = timeA[0] * 60 + timeA[1];
2393*1d05cddcSAtari911                const minutesB = timeB[0] * 60 + timeB[1];
2394*1d05cddcSAtari911
2395*1d05cddcSAtari911                return minutesA - minutesB;
2396*1d05cddcSAtari911            });
2397*1d05cddcSAtari911
2398*1d05cddcSAtari911            // Build events HTML with dual color bars
2399*1d05cddcSAtari911            sortedEvents.forEach(event => {
2400*1d05cddcSAtari911                const eventColor = event.color || "#00cc07";
2401*1d05cddcSAtari911                const sectionColor = "#3498db"; // Blue for selected day
2402*1d05cddcSAtari911
2403*1d05cddcSAtari911                const eventDiv = document.createElement("div");
2404*1d05cddcSAtari911                eventDiv.style.cssText = "padding:4px 6px; border-bottom:1px solid rgba(0, 204, 7, 0.2); font-size:10px; display:flex; align-items:start; gap:6px; background:rgba(36, 36, 36, 0.3);";
2405*1d05cddcSAtari911
2406*1d05cddcSAtari911                let eventHTML = "";
2407*1d05cddcSAtari911
2408*1d05cddcSAtari911                // Section color bar (wider, blue for selected day)
2409*1d05cddcSAtari911                eventHTML += "<div style=\\"width:4px; height:100%; background:" + sectionColor + "; border-radius:2px; flex-shrink:0; box-shadow:0 0 4px " + sectionColor + ";\\"></div>";
2410*1d05cddcSAtari911
2411*1d05cddcSAtari911                // Event assigned color bar (medium width)
2412*1d05cddcSAtari911                eventHTML += "<div style=\\"width:3px; height:100%; background:" + eventColor + "; border-radius:1px; flex-shrink:0; box-shadow:0 0 3px " + eventColor + ";\\"></div>";
2413*1d05cddcSAtari911
2414*1d05cddcSAtari911                // Content
2415*1d05cddcSAtari911                eventHTML += "<div style=\\"flex:1; min-width:0;\\">";
2416*1d05cddcSAtari911                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);\\">";
2417*1d05cddcSAtari911
2418*1d05cddcSAtari911                // Time
2419*1d05cddcSAtari911                if (event.time) {
2420*1d05cddcSAtari911                    const timeParts = event.time.split(":");
2421*1d05cddcSAtari911                    let hours = parseInt(timeParts[0]);
2422*1d05cddcSAtari911                    const minutes = timeParts[1];
2423*1d05cddcSAtari911                    const ampm = hours >= 12 ? "PM" : "AM";
2424*1d05cddcSAtari911                    hours = hours % 12 || 12;
2425*1d05cddcSAtari911                    eventHTML += "<span style=\\"color:#00dd00; font-weight:500; font-size:9px;\\">" + hours + ":" + minutes + " " + ampm + "</span> ";
2426*1d05cddcSAtari911                }
2427*1d05cddcSAtari911
2428*1d05cddcSAtari911                // Title - use HTML version if available
2429*1d05cddcSAtari911                const titleHTML = event.title_html || event.title || "Untitled";
2430*1d05cddcSAtari911                eventHTML += titleHTML;
2431*1d05cddcSAtari911                eventHTML += "</div>";
2432*1d05cddcSAtari911
2433*1d05cddcSAtari911                // Description if present - use HTML version
2434*1d05cddcSAtari911                if (event.description_html || event.description) {
2435*1d05cddcSAtari911                    const descHTML = event.description_html || event.description;
2436*1d05cddcSAtari911                    eventHTML += "<div style=\\"font-size:9px; color:#00aa00; margin-top:2px;\\">" + descHTML + "</div>";
2437*1d05cddcSAtari911                }
2438*1d05cddcSAtari911
2439*1d05cddcSAtari911                eventHTML += "</div>";
2440*1d05cddcSAtari911
2441*1d05cddcSAtari911                eventDiv.innerHTML = eventHTML;
2442*1d05cddcSAtari911                content.appendChild(eventDiv);
2443*1d05cddcSAtari911            });
2444*1d05cddcSAtari911
2445*1d05cddcSAtari911            container.style.display = "block";
2446*1d05cddcSAtari911        };
2447*1d05cddcSAtari911        ';
2448*1d05cddcSAtari911        $html .= '</script>';
2449*1d05cddcSAtari911
2450*1d05cddcSAtari911        return $html;
2451*1d05cddcSAtari911    }
2452*1d05cddcSAtari911
2453*1d05cddcSAtari911    /**
2454*1d05cddcSAtari911     * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders
2455*1d05cddcSAtari911     */
2456*1d05cddcSAtari911    private function renderSidebarSection($title, $events, $accentColor, $calId) {
2457*1d05cddcSAtari911        // Keep the original accent colors for borders
2458*1d05cddcSAtari911        $borderColor = $accentColor;
2459*1d05cddcSAtari911
2460*1d05cddcSAtari911        // Show date for Important Events section
2461*1d05cddcSAtari911        $showDate = ($title === 'Important Events');
2462*1d05cddcSAtari911
2463*1d05cddcSAtari911        $html = '<div style="border-left:3px solid ' . $borderColor . '; margin:8px 4px; box-shadow:0 0 5px rgba(0, 204, 7, 0.2);">';
2464*1d05cddcSAtari911
2465*1d05cddcSAtari911        // Section header with accent color background - smaller, not all caps
2466*1d05cddcSAtari911        $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 . ';">';
2467*1d05cddcSAtari911        $html .= htmlspecialchars($title);
2468*1d05cddcSAtari911        $html .= '</div>';
2469*1d05cddcSAtari911
2470*1d05cddcSAtari911        // Events
2471*1d05cddcSAtari911        $html .= '<div style="padding:4px 0; background:rgba(36, 36, 36, 0.5);">';
2472*1d05cddcSAtari911
2473*1d05cddcSAtari911        foreach ($events as $event) {
2474*1d05cddcSAtari911            $html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor);
2475*1d05cddcSAtari911        }
2476*1d05cddcSAtari911
2477*1d05cddcSAtari911        $html .= '</div>';
2478*1d05cddcSAtari911        $html .= '</div>';
2479*1d05cddcSAtari911
2480*1d05cddcSAtari911        return $html;
2481*1d05cddcSAtari911    }
2482*1d05cddcSAtari911
2483*1d05cddcSAtari911    /**
2484*1d05cddcSAtari911     * Render individual event in sidebar - Matrix themed with dual color bars
2485*1d05cddcSAtari911     */
2486*1d05cddcSAtari911    private function renderSidebarEvent($event, $calId, $showDate = false, $sectionColor = '#00cc07') {
2487*1d05cddcSAtari911        $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
2488*1d05cddcSAtari911        $time = isset($event['time']) ? $event['time'] : '';
2489*1d05cddcSAtari911        $endTime = isset($event['endTime']) ? $event['endTime'] : '';
2490*1d05cddcSAtari911        $eventColor = isset($event['color']) ? htmlspecialchars($event['color']) : '#00cc07';
2491*1d05cddcSAtari911        $date = isset($event['date']) ? $event['date'] : '';
2492*1d05cddcSAtari911        $isTask = isset($event['isTask']) && $event['isTask'];
2493*1d05cddcSAtari911        $completed = isset($event['completed']) && $event['completed'];
2494*1d05cddcSAtari911
2495*1d05cddcSAtari911        // Check for conflicts
2496*1d05cddcSAtari911        $hasConflict = isset($event['conflicts']) && !empty($event['conflicts']);
2497*1d05cddcSAtari911
2498*1d05cddcSAtari911        $html = '<div style="padding:4px 6px; border-bottom:1px solid rgba(0, 204, 7, 0.2); font-size:10px; display:flex; align-items:start; gap:6px; background:rgba(36, 36, 36, 0.3);">';
2499*1d05cddcSAtari911
2500*1d05cddcSAtari911        // Section color bar (wider, on the left)
2501*1d05cddcSAtari911        $html .= '<div style="width:4px; height:100%; background:' . htmlspecialchars($sectionColor) . '; border-radius:2px; flex-shrink:0; box-shadow:0 0 4px ' . htmlspecialchars($sectionColor) . ';"></div>';
2502*1d05cddcSAtari911
2503*1d05cddcSAtari911        // Event's assigned color bar (medium width, next to section bar)
2504*1d05cddcSAtari911        $html .= '<div style="width:3px; height:100%; background:' . $eventColor . '; border-radius:1px; flex-shrink:0; box-shadow:0 0 3px ' . $eventColor . ';"></div>';
2505*1d05cddcSAtari911
2506*1d05cddcSAtari911        // Content
2507*1d05cddcSAtari911        $html .= '<div style="flex:1; min-width:0;">';
2508*1d05cddcSAtari911
2509*1d05cddcSAtari911        // Time + title
2510*1d05cddcSAtari911        $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);">';
2511*1d05cddcSAtari911
2512*1d05cddcSAtari911        if ($time) {
2513*1d05cddcSAtari911            $displayTime = $this->formatTimeDisplay($time, $endTime);
2514*1d05cddcSAtari911            $html .= '<span style="color:#00dd00; font-weight:500; font-size:9px;">' . htmlspecialchars($displayTime) . '</span> ';
2515*1d05cddcSAtari911        }
2516*1d05cddcSAtari911
2517*1d05cddcSAtari911        // Task checkbox
2518*1d05cddcSAtari911        if ($isTask) {
2519*1d05cddcSAtari911            $checkIcon = $completed ? '☑' : '☐';
2520*1d05cddcSAtari911            $html .= '<span style="font-size:11px; color:#00ff00;">' . $checkIcon . '</span> ';
2521*1d05cddcSAtari911        }
2522*1d05cddcSAtari911
2523*1d05cddcSAtari911        $html .= htmlspecialchars($title);
2524*1d05cddcSAtari911
2525*1d05cddcSAtari911        // Conflict badge
2526*1d05cddcSAtari911        if ($hasConflict) {
2527*1d05cddcSAtari911            $conflictCount = count($event['conflicts']);
2528*1d05cddcSAtari911            $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>';
2529*1d05cddcSAtari911        }
2530*1d05cddcSAtari911
2531*1d05cddcSAtari911        $html .= '</div>';
2532*1d05cddcSAtari911
2533*1d05cddcSAtari911        // Date display BELOW event name for Important events
2534*1d05cddcSAtari911        if ($showDate && $date) {
2535*1d05cddcSAtari911            $dateObj = new DateTime($date);
2536*1d05cddcSAtari911            $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10"
2537*1d05cddcSAtari911            $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>';
2538*1d05cddcSAtari911        }
2539*1d05cddcSAtari911
2540*1d05cddcSAtari911        $html .= '</div>';
2541*1d05cddcSAtari911        $html .= '</div>';
2542*1d05cddcSAtari911
2543*1d05cddcSAtari911        return $html;
2544*1d05cddcSAtari911    }
2545*1d05cddcSAtari911
2546*1d05cddcSAtari911    /**
2547*1d05cddcSAtari911     * Format time display (12-hour format with optional end time)
2548*1d05cddcSAtari911     */
2549*1d05cddcSAtari911    private function formatTimeDisplay($startTime, $endTime = '') {
2550*1d05cddcSAtari911        // Convert start time
2551*1d05cddcSAtari911        list($hour, $minute) = explode(':', $startTime);
2552*1d05cddcSAtari911        $hour = (int)$hour;
2553*1d05cddcSAtari911        $ampm = $hour >= 12 ? 'PM' : 'AM';
2554*1d05cddcSAtari911        $displayHour = $hour % 12;
2555*1d05cddcSAtari911        if ($displayHour === 0) $displayHour = 12;
2556*1d05cddcSAtari911
2557*1d05cddcSAtari911        $display = $displayHour . ':' . $minute . ' ' . $ampm;
2558*1d05cddcSAtari911
2559*1d05cddcSAtari911        // Add end time if provided
2560*1d05cddcSAtari911        if ($endTime && $endTime !== '') {
2561*1d05cddcSAtari911            list($endHour, $endMinute) = explode(':', $endTime);
2562*1d05cddcSAtari911            $endHour = (int)$endHour;
2563*1d05cddcSAtari911            $endAmpm = $endHour >= 12 ? 'PM' : 'AM';
2564*1d05cddcSAtari911            $endDisplayHour = $endHour % 12;
2565*1d05cddcSAtari911            if ($endDisplayHour === 0) $endDisplayHour = 12;
2566*1d05cddcSAtari911
2567*1d05cddcSAtari911            $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm;
2568*1d05cddcSAtari911        }
2569*1d05cddcSAtari911
2570*1d05cddcSAtari911        return $display;
2571*1d05cddcSAtari911    }
2572*1d05cddcSAtari911
2573*1d05cddcSAtari911    /**
2574*1d05cddcSAtari911     * Render DokuWiki syntax to HTML
2575*1d05cddcSAtari911     * Converts **bold**, //italic//, [[links]], etc. to HTML
2576*1d05cddcSAtari911     */
2577*1d05cddcSAtari911    private function renderDokuWikiToHtml($text) {
2578*1d05cddcSAtari911        if (empty($text)) return '';
2579*1d05cddcSAtari911
2580*1d05cddcSAtari911        // Use DokuWiki's parser to render the text
2581*1d05cddcSAtari911        $instructions = p_get_instructions($text);
2582*1d05cddcSAtari911
2583*1d05cddcSAtari911        // Render instructions to XHTML
2584*1d05cddcSAtari911        $xhtml = p_render('xhtml', $instructions, $info);
2585*1d05cddcSAtari911
2586*1d05cddcSAtari911        // Remove surrounding <p> tags if present (we're rendering inline)
2587*1d05cddcSAtari911        $xhtml = preg_replace('/^<p>(.*)<\/p>$/s', '$1', trim($xhtml));
2588*1d05cddcSAtari911
2589*1d05cddcSAtari911        return $xhtml;
2590*1d05cddcSAtari911    }
2591*1d05cddcSAtari911
2592*1d05cddcSAtari911    // Keep old scanForNamespaces for backward compatibility (not used anymore)
2593*1d05cddcSAtari911    private function scanForNamespaces($dir, $baseNamespace, &$namespaces) {
2594*1d05cddcSAtari911        if (!is_dir($dir)) return;
2595*1d05cddcSAtari911
2596*1d05cddcSAtari911        $items = scandir($dir);
2597*1d05cddcSAtari911        foreach ($items as $item) {
2598*1d05cddcSAtari911            if ($item === '.' || $item === '..' || $item === 'calendar') continue;
2599*1d05cddcSAtari911
2600*1d05cddcSAtari911            $path = $dir . $item;
2601*1d05cddcSAtari911            if (is_dir($path)) {
2602*1d05cddcSAtari911                $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
2603*1d05cddcSAtari911                $namespaces[] = $namespace;
2604*1d05cddcSAtari911                $this->scanForNamespaces($path . '/', $namespace, $namespaces);
2605*1d05cddcSAtari911            }
2606*1d05cddcSAtari911        }
2607*1d05cddcSAtari911    }
260819378907SAtari911}
2609