<?php

declare(strict_types=1);

const DB_HOST = 'localhost';
const DB_PORT = 3306;
const DB_NAME = 'ev';
const DB_USER = 'root';
const DB_PASS = 'jV9ELLV7!6rNm8GjV9ELLV7!6rNm8G';
const MAX_LIMIT = 200;
const DEFAULT_AUTO_REFRESH_SECONDS = 10;
const MAX_AUTO_REFRESH_SECONDS = 3600;
const SORT_BY_DEFAULT = 'id';
const SORT_DIR_DEFAULT = 'desc';

const TABLE_FIELDS = [
    'HomeBeaconLoc',
    'HomeWIFILoc',
    'SmartLoc',
    'FallDown',
    'Indoors',
    'FallDownAlert',
    'TiltAlert',
    'NoMotionAlert',
    'SOSKeyAlert',
    'Latitude',
    'Longitude',
    'BeaconLocation',
    'BeaconLocation2',
    'NewWiFiLocations',
];

function h(mixed $value): string
{
    return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function getPdo(): PDO
{
    $dsn = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME . ';charset=utf8mb4';

    return new PDO($dsn, DB_USER, DB_PASS, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);
}

function flattenArray(array $data, string $prefix = ''): array
{
    $result = [];

    foreach ($data as $key => $value) {
        $newKey = $prefix === '' ? (string)$key : $prefix . '.' . $key;

        if (is_array($value)) {
            if ($value === []) {
                $result[$newKey] = '[]';
            } else {
                foreach (flattenArray($value, $newKey) as $childKey => $childValue) {
                    $result[$childKey] = $childValue;
                }
            }
        } elseif (is_bool($value)) {
            $result[$newKey] = $value ? 'true' : 'false';
        } elseif ($value === null) {
            $result[$newKey] = 'null';
        } else {
            $result[$newKey] = (string)$value;
        }
    }

    return $result;
}

function collectFieldMatches(array $data, string $fieldName, string $path = ''): array
{
    $matches = [];

    foreach ($data as $key => $value) {
        $currentPath = $path === '' ? (string)$key : $path . '.' . $key;

        if ((string)$key === $fieldName) {
            $matches[] = ['path' => $currentPath, 'value' => $value];
        }

        if (is_array($value)) {
            foreach (collectFieldMatches($value, $fieldName, $currentPath) as $childMatch) {
                $matches[] = $childMatch;
            }
        }
    }

    return $matches;
}

function valueToScalarString(mixed $value): string
{
    if (is_bool($value)) {
        return $value ? 'true' : 'false';
    }
    if ($value === null) {
        return 'null';
    }
    if (is_array($value)) {
        return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
    }
    return trim((string)$value);
}

function extractRequestedFields(array $decoded): array
{
    $result = [];

    foreach (TABLE_FIELDS as $fieldName) {
        $matches = collectFieldMatches($decoded, $fieldName);

        if ($matches === []) {
            $result[$fieldName] = [
                'value' => null,
                'path' => null,
            ];
            continue;
        }

        $selected = $matches[0];
        $result[$fieldName] = [
            'value' => $selected['value'],
            'path' => $selected['path'],
        ];
    }

    return $result;
}

function summarizeArrayValue(mixed $value): string
{
    if (!is_array($value)) {
        return valueToScalarString($value);
    }

    $count = count($value);
    if ($count === 0) {
        return '[]';
    }

    $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $preview = $json !== false ? mb_strimwidth($json, 0, 60, '…', 'UTF-8') : '[array]';
    return $count . ' item' . ($count === 1 ? '' : 's') . ' · ' . $preview;
}


function isAssocArray(array $value): bool
{
    return array_keys($value) !== range(0, count($value) - 1);
}

function findFirstByKeysCaseInsensitive(array $data, array $keys): mixed
{
    $lookup = array_map('strtolower', $keys);

    foreach ($data as $key => $value) {
        if (in_array(strtolower((string)$key), $lookup, true)) {
            return $value;
        }
    }

    return null;
}

function normalizeMacAddress(mixed $value): ?string
{
    if (!is_string($value) && !is_numeric($value)) {
        return null;
    }

    $value = strtoupper(trim((string)$value));
    if ($value === '') {
        return null;
    }

    $value = str_replace(['-', '.'], ':', $value);
    if (preg_match('/^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$/', $value) === 1) {
        return $value;
    }

    if (preg_match('/^[0-9A-F]{12}$/', $value) === 1) {
        return implode(':', str_split($value, 2));
    }

    return null;
}

function normalizeRssiValue(mixed $value): ?string
{
    if ($value === null || $value === '') {
        return null;
    }

    if (is_bool($value) || is_array($value)) {
        return null;
    }

    $string = trim((string)$value);
    if ($string === '') {
        return null;
    }

    if (is_numeric($string)) {
        return (string)(int)round((float)$string);
    }

    if (preg_match('/-?\d+/', $string, $matches) === 1) {
        return $matches[0];
    }

    return null;
}

function extractMacRssiPairs(mixed $value): array
{
    $pairs = [];

    if (!is_array($value)) {
        return $pairs;
    }

    if (isAssocArray($value)) {
        $mac = normalizeMacAddress(findFirstByKeysCaseInsensitive($value, ['MacAddress', 'MAC', 'Mac', 'BSSID', 'Address', 'Addr']));
        $rssi = normalizeRssiValue(findFirstByKeysCaseInsensitive($value, ['RSSI', 'Rssi', 'Signal', 'SignalStrength', 'dBm', 'Dbm']));

        if ($mac !== null) {
            $pairs[] = [
                'mac' => $mac,
                'rssi' => $rssi,
            ];
        }
    }

    foreach ($value as $child) {
        if (is_array($child)) {
            foreach (extractMacRssiPairs($child) as $pair) {
                $pairs[] = $pair;
            }
        }
    }

    $unique = [];
    foreach ($pairs as $pair) {
        $key = $pair['mac'] . '|' . ($pair['rssi'] ?? '');
        $unique[$key] = $pair;
    }

    return array_values($unique);
}

function getLatestDisplayedRow(array $rows): ?array
{
    if ($rows === []) {
        return null;
    }

    $latest = null;
    foreach ($rows as $row) {
        if ($latest === null || (int)($row['id'] ?? 0) > (int)($latest['id'] ?? 0)) {
            $latest = $row;
        }
    }

    return $latest;
}

function buildSignalSummary(array $rows): array
{
    $latestRow = getLatestDisplayedRow($rows);

    if ($latestRow === null) {
        return [
            'latest_id' => null,
            'beacons' => [],
            'wifi' => [],
        ];
    }

    $requested = $latestRow['requested_fields'] ?? [];
    $beacons = [];
    foreach (['BeaconLocation', 'BeaconLocation2'] as $fieldName) {
        foreach (extractMacRssiPairs($requested[$fieldName]['value'] ?? null) as $pair) {
            $beacons[] = $pair;
        }
    }

    $wifi = extractMacRssiPairs($requested['NewWiFiLocations']['value'] ?? null);

    $dedupe = static function (array $items): array {
        $unique = [];
        foreach ($items as $item) {
            $key = ($item['mac'] ?? '') . '|' . ($item['rssi'] ?? '');
            $unique[$key] = $item;
        }
        return array_values($unique);
    };

    return [
        'latest_id' => (int)($latestRow['id'] ?? 0),
        'beacons' => $dedupe($beacons),
        'wifi' => $dedupe($wifi),
    ];
}

function renderSignalItems(array $items, string $emptyLabel): string
{
    if ($items === []) {
        return '<div class="signal-empty">' . h($emptyLabel) . '</div>';
    }

    $html = '<div class="signal-list">';
    foreach ($items as $item) {
        $html .= '<div class="signal-item">';
        $html .= '<span class="signal-mac mono">' . h($item['mac'] ?? '') . '</span>';
        $html .= '<span class="signal-rssi">RSSI ' . h(($item['rssi'] ?? '—')) . '</span>';
        $html .= '</div>';
    }
    $html .= '</div>';

    return $html;
}

function normalizeCoordinate(mixed $value): ?string
{
    if ($value === null || $value === '') {
        return null;
    }

    if (is_array($value) || is_bool($value)) {
        return null;
    }

    $string = trim((string)$value);
    if ($string === '' || strtolower($string) === 'null') {
        return null;
    }

    if (!is_numeric($string)) {
        return null;
    }

    return $string;
}

function buildMapUrl(?string $lat, ?string $lon): ?string
{
    if ($lat === null || $lon === null) {
        return null;
    }

    return 'https://www.openstreetmap.org/?mlat=' . rawurlencode($lat) . '&mlon=' . rawurlencode($lon) . '#map=16/' . rawurlencode($lat) . '/' . rawurlencode($lon);
}

function buildMapEmbedUrl(?string $lat, ?string $lon): ?string
{
    if ($lat === null || $lon === null) {
        return null;
    }

    $latFloat = (float)$lat;
    $lonFloat = (float)$lon;
    $delta = 0.0045;
    $left = $lonFloat - $delta;
    $right = $lonFloat + $delta;
    $top = $latFloat + $delta;
    $bottom = $latFloat - $delta;

    return 'https://www.openstreetmap.org/export/embed.html?bbox='
        . rawurlencode((string)$left . ',' . (string)$bottom . ',' . (string)$right . ',' . (string)$top)
        . '&layer=mapnik&marker=' . rawurlencode((string)$latFloat . ',' . (string)$lonFloat);
}

function getSortByOptions(): array
{
    return [
        'id' => 'ID',
        'insert_time' => 'Insert time',
    ];
}

function normalizeSortBy(string $sortBy): string
{
    $allowed = array_keys(getSortByOptions());
    return in_array($sortBy, $allowed, true) ? $sortBy : SORT_BY_DEFAULT;
}

function normalizeSortDir(string $sortDir): string
{
    $sortDir = strtolower(trim($sortDir));
    return in_array($sortDir, ['asc', 'desc'], true) ? $sortDir : SORT_DIR_DEFAULT;
}

function csvValue(mixed $value): string
{
    if (is_array($value)) {
        return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
    }
    if (is_bool($value)) {
        return $value ? 'true' : 'false';
    }
    if ($value === null) {
        return '';
    }
    return trim((string)$value);
}

function outputCsv(array $rows, string $imei): void
{
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename="device_messages_' . preg_replace('/[^A-Za-z0-9_-]+/', '_', $imei) . '_' . date('Ymd_His') . '.csv"');

    $out = fopen('php://output', 'wb');
    if ($out === false) {
        throw new RuntimeException('Impossibile generare il CSV.');
    }

    $header = ['id', 'insert_time', 'IMEI'];
    foreach (TABLE_FIELDS as $fieldName) {
        $header[] = $fieldName;
    }
    $header[] = 'severity';
    $header[] = 'detail_fields_json';
    $header[] = 'raw_text';
    fputcsv($out, $header);

    foreach ($rows as $row) {
        $line = [
            $row['id'] ?? '',
            $row['insert_time'] ?? '',
            $row['IMEI'] ?? '',
        ];

        foreach (TABLE_FIELDS as $fieldName) {
            $line[] = csvValue($row['requested_fields'][$fieldName]['value'] ?? null);
        }

        $line[] = $row['severity'] ?? '';
        $line[] = json_encode($row['detail_fields'] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}';
        $line[] = $row['text'] ?? '';
        fputcsv($out, $line);
    }

    fclose($out);
}

function isTruthyFlag(mixed $value): bool
{
    if ($value === true) {
        return true;
    }

    if (is_string($value)) {
        return strtolower(trim($value)) === 'true' || trim($value) === '1';
    }

    if (is_int($value) || is_float($value)) {
        return (float)$value === 1.0;
    }

    return false;
}

function hasActiveAlert(array $requestedFields): bool
{
    $alertFields = ['FallDownAlert', 'TiltAlert', 'NoMotionAlert', 'SOSKeyAlert'];

    foreach ($alertFields as $field) {
        $value = $requestedFields[$field]['value'] ?? null;
        if (isTruthyFlag($value)) {
            return true;
        }
    }

    return false;
}

function detectSeverity(array $requestedFields, array $flat, string $rawText): string
{
    if (hasActiveAlert($requestedFields)) {
        return 'danger';
    }

    $fallDown = $requestedFields['FallDown']['value'] ?? null;
    if (isTruthyFlag($fallDown)) {
        return 'warning';
    }

    $haystack = strtolower($rawText . ' ' . implode(' ', array_map('strval', $flat)));
    if (str_contains($haystack, 'charging') || str_contains($haystack, 'motion') || str_contains($haystack, 'gps')) {
        return 'info';
    }

    return 'neutral';
}

function buildRows(PDO $pdo, string $imei, int $limit, string $sortBy, string $sortDir): array
{
    $orderColumn = $sortBy === 'insert_time' ? 'dm.insert_time' : 'dm.id';
    $orderDirection = strtoupper($sortDir) === 'ASC' ? 'ASC' : 'DESC';

    $sql = "
        SELECT
            dm.id,
            dm.insert_time,
            dm.text,
            d.IMEI
        FROM device_messages dm
        INNER JOIN devices d ON d.id = dm.id_devices
        WHERE d.IMEI = :imei
        ORDER BY {$orderColumn} {$orderDirection}, dm.id DESC
        LIMIT :row_limit
    ";

    $stmt = $pdo->prepare($sql);
    $stmt->bindValue(':imei', $imei, PDO::PARAM_STR);
    $stmt->bindValue(':row_limit', $limit, PDO::PARAM_INT);
    $stmt->execute();

    $rows = $stmt->fetchAll();

    foreach ($rows as &$row) {
        $decoded = json_decode((string)$row['text'], true);
        $isJson = json_last_error() === JSON_ERROR_NONE && is_array($decoded);
        $flat = $isJson ? flattenArray($decoded) : [];
        $requestedFields = $isJson ? extractRequestedFields($decoded) : [];

        $lat = normalizeCoordinate($requestedFields['Latitude']['value'] ?? null);
        $lon = normalizeCoordinate($requestedFields['Longitude']['value'] ?? null);

        $detailFields = [];
        if ($isJson) {
            foreach ($flat as $path => $value) {
                $lastKey = (string)substr($path, (int)strrpos('.' . $path, '.'));
                $lastKey = ltrim($lastKey, '.');
                if (in_array($lastKey, TABLE_FIELDS, true)) {
                    continue;
                }
                $detailFields[$path] = $value;
            }
        }

        $row['json_valid'] = $isJson;
        $row['flat'] = $flat;
        $row['requested_fields'] = $requestedFields;
        $row['has_active_alert'] = hasActiveAlert($requestedFields);
        $row['severity'] = detectSeverity($requestedFields, $flat, (string)$row['text']);
        $row['raw_preview'] = mb_strimwidth(trim((string)$row['text']), 0, 140, '…', 'UTF-8');
        $row['lat'] = $lat;
        $row['lon'] = $lon;
        $row['map_url'] = buildMapUrl($lat, $lon);
        $row['map_embed_url'] = buildMapEmbedUrl($lat, $lon);
        $row['detail_fields'] = $detailFields;
    }
    unset($row);

    return $rows;
}

function renderFieldCell(array $row, string $fieldName): string
{
    $alertFields = ['FallDownAlert', 'TiltAlert', 'NoMotionAlert', 'SOSKeyAlert'];
    $field = $row['requested_fields'][$fieldName] ?? ['value' => null, 'path' => null];
    $value = $field['value'];
    $path = $field['path'];
    $title = $path ? 'Percorso JSON: ' . $path : 'Campo non presente';

    if (in_array($fieldName, ['Latitude', 'Longitude'], true)) {
        $coordValue = $fieldName === 'Latitude' ? ($row['lat'] ?? null) : ($row['lon'] ?? null);
        if ($coordValue !== null) {
            return '<span class="coord-chip mono" title="' . h($title) . '">' . h($coordValue) . '</span>';
        }
        return '<span class="muted-cell" title="' . h($title) . '">—</span>';
    }

    if (in_array($fieldName, ['BeaconLocation', 'BeaconLocation2', 'NewWiFiLocations'], true)) {
        if ($value === null) {
            return '<span class="muted-cell" title="' . h($title) . '">[]</span>';
        }
        return '<span class="array-chip" title="' . h($title . ' | ' . summarizeArrayValue($value)) . '">' . h(summarizeArrayValue($value)) . '</span>';
    }

    if (is_bool($value)) {
        $isAlertField = in_array($fieldName, $alertFields, true);
        $class = $value ? ($isAlertField ? 'bool-alert-true' : 'bool-true') : 'bool-false';
        $label = $value ? 'true' : 'false';
        return '<span class="bool-badge ' . $class . '" title="' . h($title) . '">' . $label . '</span>';
    }

    if ($value === null) {
        return '<span class="muted-cell" title="' . h($title) . '">—</span>';
    }

    return '<span title="' . h($title) . '">' . h(valueToScalarString($value)) . '</span>';
}

function renderRowsHtml(array $rows, bool $onlyAlerts = false): string
{
    if ($rows === []) {
        return '<div class="empty-state">Nessun messaggio trovato per l\'IMEI selezionato.</div>';
    }

    $dangerCount = count(array_filter($rows, static fn(array $r): bool => $r['severity'] === 'danger'));
    $jsonCount = count(array_filter($rows, static fn(array $r): bool => $r['json_valid']));
    $geoCount = count(array_filter($rows, static fn(array $r): bool => !empty($r['map_url'])));

    ob_start();
    ?>
    <div class="results-toolbar">
        <div class="metric-card">
            <span class="metric-label">Messaggi</span>
            <strong><?= count($rows) ?></strong>
        </div>
        <div class="metric-card">
            <span class="metric-label">JSON validi</span>
            <strong><?= $jsonCount ?></strong>
        </div>
        <div class="metric-card <?= $dangerCount > 0 ? 'danger-outline' : '' ?>">
            <span class="metric-label">Eventi critici</span>
            <strong><?= $dangerCount ?></strong>
        </div>
        <div class="metric-card">
            <span class="metric-label">Coordinate disponibili</span>
            <strong><?= $geoCount ?></strong>
        </div>
        <div class="metric-card <?= $onlyAlerts ? 'danger-outline' : '' ?>">
            <span class="metric-label">Filtro alert</span>
            <strong><?= $onlyAlerts ? 'Solo attivi' : 'Tutti' ?></strong>
        </div>
    </div>

    <div class="table-shell">
        <table class="log-table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Insert time</th>
                    <th>IMEI</th>
                    <?php foreach (TABLE_FIELDS as $fieldName): ?>
                        <th><?= h($fieldName) ?></th>
                    <?php endforeach; ?>
                    <th>Dettagli</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($rows as $index => $row): ?>
                    <tr class="severity-row-<?= h($row['severity']) ?>" data-row-id="<?= h((string)$row['id']) ?>">
                        <td class="mono" data-label="ID"><?= h($row['id']) ?></td>
                        <td class="mono" data-label="Insert time"><?= h($row['insert_time']) ?></td>
                        <td class="mono" data-label="IMEI"><?= h($row['IMEI']) ?></td>
                        <?php foreach (TABLE_FIELDS as $fieldName): ?>
                            <td class="truncate-cell" data-label="<?= h($fieldName) ?>"><?= renderFieldCell($row, $fieldName) ?></td>
                        <?php endforeach; ?>
                        <td data-label="Dettagli">
                            <button type="button" class="toggle-details" data-target="details-<?= h((string)$row['id']) ?>-<?= $index ?>">Apri</button>
                        </td>
                    </tr>
                    <tr id="details-<?= h((string)$row['id']) ?>-<?= $index ?>" class="details-row" hidden>
                        <td colspan="<?= 4 + count(TABLE_FIELDS) ?>">
                            <div class="details-grid">
                                <div class="details-panel">
                                    <div class="panel-title">Altri campi del JSON</div>
                                    <?php if ($row['json_valid'] && $row['detail_fields'] !== []): ?>
                                        <div class="details-table-wrap">
                                            <table class="details-table">
                                                <thead>
                                                    <tr>
                                                        <th>Percorso</th>
                                                        <th>Valore</th>
                                                    </tr>
                                                </thead>
                                                <tbody>
                                                    <?php foreach ($row['detail_fields'] as $key => $value): ?>
                                                        <tr>
                                                            <td class="mono"><?= h($key) ?></td>
                                                            <td><?= h($value) ?></td>
                                                        </tr>
                                                    <?php endforeach; ?>
                                                </tbody>
                                            </table>
                                        </div>
                                    <?php else: ?>
                                        <div class="empty-inline">Nessun altro campo da mostrare nei dettagli.</div>
                                    <?php endif; ?>
                                </div>

                                <div class="details-panel">
                                    <div class="panel-title">Mini mappa</div>
                                    <?php if (!empty($row['map_embed_url'])): ?>
                                        <div class="map-card">
                                            <iframe
                                                class="map-frame"
                                                loading="lazy"
                                                referrerpolicy="no-referrer-when-downgrade"
                                                src="<?= h($row['map_embed_url']) ?>"></iframe>
                                            <div class="map-actions">
                                                <span class="coord-inline mono">Lat <?= h($row['lat']) ?> · Lon <?= h($row['lon']) ?></span>
                                                <a class="map-link" href="<?= h($row['map_url']) ?>" target="_blank" rel="noopener noreferrer">Apri in OpenStreetMap</a>
                                            </div>
                                        </div>
                                    <?php else: ?>
                                        <div class="empty-inline">Coordinate non disponibili per mostrare la mappa.</div>
                                    <?php endif; ?>
                                </div>

                                <div class="details-panel">
                                    <div class="panel-title">Raw completo</div>
                                    <pre class="raw-block"><?= h($row['text']) ?></pre>
                                </div>
                            </div>
                        </td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    </div>
    <?php
    return (string)ob_get_clean();
}

$imei = isset($_GET['imei']) ? trim((string)$_GET['imei']) : '';
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
$limit = max(1, min(MAX_LIMIT, $limit));
$refreshSeconds = isset($_GET['refresh_seconds']) ? (int)$_GET['refresh_seconds'] : DEFAULT_AUTO_REFRESH_SECONDS;
$refreshSeconds = max(0, min(MAX_AUTO_REFRESH_SECONDS, $refreshSeconds));
$onlyAlerts = isset($_GET['only_alerts']) && $_GET['only_alerts'] === '1';
$sortBy = normalizeSortBy(isset($_GET['sort_by']) ? (string)$_GET['sort_by'] : SORT_BY_DEFAULT);
$sortDir = normalizeSortDir(isset($_GET['sort_dir']) ? (string)$_GET['sort_dir'] : SORT_DIR_DEFAULT);
$isAjax = isset($_GET['ajax']) && $_GET['ajax'] === '1';
$isExportCsv = isset($_GET['export']) && $_GET['export'] === 'csv';
$sortOptions = getSortByOptions();

$error = '';
$html = '';
$rows = [];
$signalSummary = [
    'latest_id' => null,
    'beacons' => [],
    'wifi' => [],
];

if ($imei !== '') {
    try {
        $pdo = getPdo();
        $rows = buildRows($pdo, $imei, $limit, $sortBy, $sortDir);
        if ($onlyAlerts) {
            $rows = array_values(array_filter($rows, static fn(array $row): bool => !empty($row['has_active_alert'])));
        }
        if ($isExportCsv) {
            outputCsv($rows, $imei);
            exit;
        }
        $signalSummary = buildSignalSummary($rows);
        $html = renderRowsHtml($rows, $onlyAlerts);
    } catch (Throwable $e) {
        $error = $e->getMessage();
    }
}

if ($isAjax) {
    header('Content-Type: application/json; charset=utf-8');
    $rowIds = [];
    if (isset($rows) && is_array($rows)) {
        $rowIds = array_map(static fn(array $row): int => (int)$row['id'], $rows);
    }

    $signalSummary = buildSignalSummary($rows);

    echo json_encode([
        'ok' => $error === '',
        'html' => $html,
        'error' => $error,
        'refreshed_at' => date('Y-m-d H:i:s'),
        'row_ids' => $rowIds,
        'sort_by' => $sortBy,
        'sort_dir' => $sortDir,
        'signal_summary' => $signalSummary,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}
?>
<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Monitor PRO device_messages</title>
    <style>
        :root {
            --bg: #081120;
            --panel: #0f1b2d;
            --panel-2: #12233a;
            --line: #24405f;
            --line-soft: rgba(113, 145, 183, 0.25);
            --text: #eaf2fb;
            --muted: #a9bdd3;
            --accent: #22c55e;
            --accent-2: #16a34a;
            --danger: #ef4444;
            --danger-soft: rgba(239, 68, 68, 0.14);
            --warning-soft: rgba(245, 158, 11, 0.12);
            --info: #38bdf8;
            --neutral: #94a3b8;
            --shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
            --radius: 20px;
        }

        * { box-sizing: border-box; }

        body {
            margin: 0;
            font-family: Inter, Arial, Helvetica, sans-serif;
            background: linear-gradient(180deg, #04101d 0%, #081120 100%);
            color: var(--text);
        }

        .container {
            width: min(1800px, calc(100% - 24px));
            margin: 12px auto 24px;
        }

        .top-card {
            background: rgba(15, 27, 45, 0.96);
            border: 1px solid var(--line-soft);
            border-radius: var(--radius);
            box-shadow: var(--shadow);
            padding: 20px;
            margin-bottom: 16px;
        }

        h1 {
            margin: 0 0 8px;
            font-size: clamp(22px, 3vw, 30px);
        }

        .subtitle {
            margin: 0 0 18px;
            color: var(--muted);
            line-height: 1.5;
        }

        form {
            display: grid;
            grid-template-columns: minmax(220px, 1.6fr) 140px 180px auto auto auto;
            gap: 12px;
            align-items: end;
        }

        .field label {
            display: block;
            font-size: 13px;
            color: var(--muted);
            margin-bottom: 6px;
        }

        .checkbox-field {
            display: flex;
            flex-direction: column;
            justify-content: center;
            gap: 10px;
            min-width: 220px;
        }

        .checkbox-label {
            display: inline-flex !important;
            align-items: center;
            gap: 9px;
            margin: 0;
            font-size: 14px !important;
            color: var(--text) !important;
            cursor: pointer;
        }

        .checkbox-label input {
            width: 16px;
            height: 16px;
            accent-color: var(--accent);
        }

        .field input {
            width: 100%;
            padding: 13px 14px;
            border-radius: 12px;
            border: 1px solid var(--line);
            background: #07111f;
            color: var(--text);
            outline: none;
            font-size: 15px;
        }

        .field input:focus {
            border-color: var(--accent);
            box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15);
        }

        button,
        .toggle-details,
        .secondary-btn {
            border: 0;
            border-radius: 12px;
            padding: 12px 16px;
            background: linear-gradient(180deg, var(--accent), var(--accent-2));
            color: white;
            font-weight: 700;
            cursor: pointer;
            font-size: 14px;
        }

        .secondary-btn {
            background: linear-gradient(180deg, #1e3a5f, #16314f);
            border: 1px solid var(--line-soft);
        }

        .toggle-details {
            padding: 8px 12px;
            font-size: 13px;
            white-space: nowrap;
        }

        .status-bar,
        .results-toolbar {
            display: flex;
            justify-content: space-between;
            gap: 12px;
            flex-wrap: wrap;
            margin-top: 16px;
        }

        .metric-card {
            background: rgba(18, 35, 58, 0.95);
            border: 1px solid var(--line-soft);
            border-radius: 14px;
            padding: 12px 14px;
            min-width: 150px;
        }

        .metric-label {
            display: block;
            color: var(--muted);
            font-size: 12px;
            margin-bottom: 6px;
            text-transform: uppercase;
            letter-spacing: .05em;
        }

        .danger-outline {
            border-color: rgba(239, 68, 68, 0.45);
            background: var(--danger-soft);
        }

        .signal-card {
            flex: 1 1 360px;
            min-width: min(100%, 320px);
        }

        .signal-box {
            display: block;
        }

        .signal-list {
            display: grid;
            gap: 8px;
            margin-top: 4px;
        }

        .signal-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
            padding: 8px 10px;
            border: 1px solid var(--line-soft);
            border-radius: 12px;
            background: rgba(7, 17, 31, 0.72);
        }

        .signal-mac {
            color: #dbeafe;
            font-size: 12px;
            word-break: break-all;
            white-space: normal;
        }

        .signal-rssi {
            color: #86efac;
            font-size: 12px;
            font-weight: 800;
            white-space: nowrap;
        }

        .signal-empty {
            color: var(--muted);
            font-size: 13px;
            margin-top: 2px;
        }

        .error-box,
        .empty-state,
        .empty-inline {
            margin-top: 18px;
            padding: 16px;
            border-radius: 14px;
            border: 1px solid rgba(239, 68, 68, 0.4);
            background: rgba(127, 29, 29, 0.25);
            color: #fecaca;
        }

        .empty-state,
        .empty-inline {
            border-color: var(--line-soft);
            background: rgba(7, 17, 31, 0.8);
            color: var(--muted);
        }

        .table-shell {
            border: 1px solid var(--line-soft);
            border-radius: var(--radius);
            overflow: auto;
            background: rgba(15, 27, 45, 0.96);
            box-shadow: var(--shadow);
        }

        .log-table,
        .details-table {
            width: 100%;
            border-collapse: collapse;
        }

        .log-table {
            min-width: 1780px;
        }

        .log-table thead th {
            position: sticky;
            top: 0;
            z-index: 3;
            background: #10233c;
        }

        .log-table th,
        .log-table td,
        .details-table th,
        .details-table td {
            border-bottom: 1px solid var(--line-soft);
            border-right: 1px solid rgba(113, 145, 183, 0.12);
            padding: 10px 12px;
            text-align: left;
            vertical-align: top;
        }

        .severity-row-danger { background: rgba(127, 29, 29, 0.10); }
        .severity-row-info { background: rgba(56, 189, 248, 0.03); }
        .new-row-flash {
            animation: pulseNewRow 2.2s ease;
            outline: 1px solid rgba(34, 197, 94, 0.55);
            box-shadow: inset 0 0 0 9999px rgba(34, 197, 94, 0.08);
        }

        @keyframes pulseNewRow {
            0% { box-shadow: inset 0 0 0 9999px rgba(34, 197, 94, 0.32); }
            100% { box-shadow: inset 0 0 0 9999px rgba(34, 197, 94, 0.00); }
        }

        .log-table tbody tr:hover:not(.details-row) {
            background: rgba(56, 189, 248, 0.06);
        }

        .details-row {
            background: rgba(8, 17, 32, 0.98);
        }

        .details-grid {
            display: grid;
            grid-template-columns: 1.1fr .9fr;
            gap: 16px;
        }

        .details-panel {
            background: rgba(7, 17, 31, 0.9);
            border: 1px solid var(--line-soft);
            border-radius: 16px;
            padding: 14px;
            min-width: 0;
        }

        .panel-title {
            font-weight: 700;
            margin-bottom: 10px;
            color: #d6e4f5;
        }

        .details-table-wrap {
            overflow: auto;
            border: 1px solid var(--line-soft);
            border-radius: 12px;
        }

        .mono {
            font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
            white-space: nowrap;
        }

        .truncate-cell {
            min-width: 112px;
            max-width: 180px;
            word-break: break-word;
        }

        .severity-badge,
        .bool-badge,
        .array-chip {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            padding: 6px 10px;
            border-radius: 999px;
            font-size: 12px;
            font-weight: 800;
            letter-spacing: .02em;
        }

        .severity-danger {
            background: var(--danger-soft);
            color: #fecaca;
            border: 1px solid rgba(239, 68, 68, 0.32);
        }

        .bool-true {
            background: rgba(34, 197, 94, 0.14);
            color: #dcfce7;
            border: 1px solid rgba(34, 197, 94, 0.28);
        }

        .bool-alert-true {
            background: rgba(239, 68, 68, 0.18);
            color: #fecaca;
            border: 1px solid rgba(239, 68, 68, 0.42);
            box-shadow: inset 0 0 0 1px rgba(239, 68, 68, 0.15);
        }

        .severity-info {
            background: rgba(56, 189, 248, 0.14);
            color: #bae6fd;
            border: 1px solid rgba(56, 189, 248, 0.28);
        }

        .severity-neutral,
        .bool-false {
            background: rgba(148, 163, 184, 0.14);
            color: #e2e8f0;
            border: 1px solid rgba(148, 163, 184, 0.25);
        }

        .array-chip {
            background: rgba(245, 158, 11, 0.10);
            color: #fde68a;
            border: 1px solid rgba(245, 158, 11, 0.22);
            max-width: 170px;
            white-space: normal;
            border-radius: 14px;
        }

        .muted-cell {
            color: var(--muted);
        }

        .map-link {
            color: #86efac;
            font-weight: 700;
            text-decoration: none;
            border-bottom: 1px dashed rgba(134, 239, 172, 0.5);
        }

        .map-link:hover {
            color: #bbf7d0;
        }

        .coord-chip {
            display: inline-flex;
            align-items: center;
            padding: 6px 10px;
            border-radius: 999px;
            background: rgba(34, 197, 94, 0.12);
            color: #bbf7d0;
            border: 1px solid rgba(34, 197, 94, 0.26);
            font-weight: 700;
        }

        .map-card {
            display: grid;
            gap: 10px;
        }

        .map-frame {
            width: 100%;
            min-height: 260px;
            border: 1px solid var(--line-soft);
            border-radius: 14px;
            background: #020817;
        }

        .map-actions {
            display: flex;
            gap: 10px;
            align-items: center;
            justify-content: space-between;
            flex-wrap: wrap;
        }

        .coord-inline {
            color: var(--muted);
        }

        .raw-block {
            margin: 0;
            white-space: pre-wrap;
            word-break: break-word;
            background: #020817;
            padding: 12px;
            border-radius: 12px;
            border: 1px solid var(--line-soft);
            max-height: 460px;
            overflow: auto;
        }

        .loading {
            opacity: 0.55;
            transition: opacity 0.2s ease;
        }

        @media (max-width: 1200px) {
            form {
                grid-template-columns: 1fr 150px 170px 1fr;
            }

            form > button,
            form > .secondary-btn {
                width: 100%;
            }
        }

        @media (max-width: 860px) {
            .container {
                width: min(100%, calc(100% - 14px));
                margin: 8px auto 18px;
            }

            .top-card {
                padding: 14px;
                border-radius: 16px;
            }

            form {
                grid-template-columns: 1fr;
            }

            .details-grid {
                grid-template-columns: 1fr;
            }

            .table-shell {
                overflow: visible;
                background: transparent;
                border: 0;
                box-shadow: none;
            }

            .log-table {
                min-width: 0;
                border-collapse: separate;
                border-spacing: 0 12px;
            }

            .log-table thead {
                display: none;
            }

            .log-table,
            .log-table tbody,
            .log-table tr,
            .log-table td {
                display: block;
                width: 100%;
            }

            .log-table tbody tr:not(.details-row) {
                background: rgba(15, 27, 45, 0.96);
                border: 1px solid var(--line-soft);
                border-radius: 16px;
                overflow: hidden;
                box-shadow: var(--shadow);
                margin-bottom: 12px;
            }

            .log-table td {
                border-right: 0;
                padding: 10px 12px;
            }

            .log-table td::before {
                content: attr(data-label);
                display: block;
                color: var(--muted);
                font-size: 11px;
                text-transform: uppercase;
                letter-spacing: .05em;
                margin-bottom: 4px;
            }

            .details-row td::before {
                display: none;
            }

            .details-row td {
                padding: 0;
                border: 0;
            }

            .truncate-cell,
            .array-chip {
                max-width: none;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="top-card">
            <h1>Monitor PRO ultime righe di <code>device_messages</code></h1>
            <p class="subtitle">
                Inserisci l'IMEI del dispositivo e il numero massimo di righe da mostrare. La tabella espone direttamente i campi richiesti
                del JSON in <code>text</code>, mette <strong>Latitude</strong> e <strong>Longitude</strong> cliccabili verso OpenStreetMap e lascia
                tutti gli altri campi nel pannello dettagli. Hai anche filtro <strong>solo alert attivi</strong>, mini mappa embedded, evidenziazione delle nuove righe, auto-scroll sulle novità, <strong>ordinamento per ID/data</strong> ed <strong>export CSV</strong> delle righe filtrate. Se imposti <strong>0</strong>, l'aggiornamento automatico viene disattivato.
            </p>

            <form id="filterForm" method="get">
                <div class="field">
                    <label for="imei">IMEI</label>
                    <input type="text" id="imei" name="imei" value="<?= h($imei) ?>" placeholder="Es. 861629050600356" required>
                </div>

                <div class="field">
                    <label for="limit">Numero righe</label>
                    <input type="number" id="limit" name="limit" min="1" max="<?= MAX_LIMIT ?>" value="<?= h($limit) ?>" required>
                </div>

                <div class="field">
                    <label for="refresh_seconds">Refresh automatico (secondi)</label>
                    <input type="number" id="refresh_seconds" name="refresh_seconds" min="0" max="<?= MAX_AUTO_REFRESH_SECONDS ?>" value="<?= h($refreshSeconds) ?>" required>
                </div>

                <div class="field">
                    <label for="sort_by">Ordina per</label>
                    <select id="sort_by" name="sort_by">
                        <?php foreach ($sortOptions as $sortKey => $sortLabel): ?>
                            <option value="<?= h($sortKey) ?>" <?= $sortBy === $sortKey ? 'selected' : '' ?>><?= h($sortLabel) ?></option>
                        <?php endforeach; ?>
                    </select>
                </div>

                <div class="field">
                    <label for="sort_dir">Direzione</label>
                    <select id="sort_dir" name="sort_dir">
                        <option value="desc" <?= $sortDir === 'desc' ? 'selected' : '' ?>>Decrescente</option>
                        <option value="asc" <?= $sortDir === 'asc' ? 'selected' : '' ?>>Crescente</option>
                    </select>
                </div>

                <div class="field checkbox-field">
                    <label class="checkbox-label"><input type="checkbox" id="only_alerts" name="only_alerts" value="1" <?= $onlyAlerts ? 'checked' : '' ?>> Solo alert attivi</label>
                    <label class="checkbox-label"><input type="checkbox" id="auto_scroll_new" checked> Auto-scroll nuove righe</label>
                </div>

                <button type="submit">Carica monitor PRO+</button>
                <button type="button" id="forceRefreshBtn" class="secondary-btn">Aggiorna adesso</button>
                <button type="submit" id="exportCsvBtn" class="secondary-btn" formaction="?" formmethod="get" name="export" value="csv">Esporta CSV</button>
            </form>

            <div class="status-bar">
                <div class="metric-card">
                    <span class="metric-label">Aggiornamento automatico</span>
                    <strong id="autoRefreshStatus"><?= $refreshSeconds > 0 ? 'Ogni ' . h($refreshSeconds) . ' secondi' : 'Disattivato' ?></strong>
                </div>
                <div class="metric-card">
                    <span class="metric-label">Ultimo refresh</span>
                    <strong id="lastRefresh"><?= h(date('Y-m-d H:i:s')) ?></strong>
                </div>
                <div class="metric-card">
                    <span class="metric-label">Modalità vista</span>
                    <strong id="filterStatus"><?= $onlyAlerts ? 'Solo alert attivi' : 'Tutti i messaggi' ?></strong>
                </div>
                <div class="metric-card signal-card">
                    <span class="metric-label">Beacon ultimo device_message<?= $signalSummary['latest_id'] ? ' · ID ' . h((string)$signalSummary['latest_id']) : '' ?></span>
                    <div id="latestBeaconSignals" class="signal-box"><?= renderSignalItems($signalSummary['beacons'], 'Nessun beacon nel messaggio più recente') ?></div>
                </div>
                <div class="metric-card signal-card">
                    <span class="metric-label">WiFi ultimo device_message<?= $signalSummary['latest_id'] ? ' · ID ' . h((string)$signalSummary['latest_id']) : '' ?></span>
                    <div id="latestWifiSignals" class="signal-box"><?= renderSignalItems($signalSummary['wifi'], 'Nessuna rete WiFi nel messaggio più recente') ?></div>
                </div>
            </div>

            <?php if ($error !== ''): ?>
                <div class="error-box" id="errorBox"><?= h($error) ?></div>
            <?php else: ?>
                <div class="error-box" id="errorBox" style="display:none;"></div>
            <?php endif; ?>
        </div>

        <div id="results"<?= $imei !== '' ? '' : ' class="empty-state"' ?>>
            <?php if ($imei === ''): ?>
                Inserisci un IMEI e premi "Carica monitor PRO" per vedere i messaggi.
            <?php else: ?>
                <?= $html ?>
            <?php endif; ?>
        </div>
    </div>

    <script>
        const form = document.getElementById('filterForm');
        const results = document.getElementById('results');
        const errorBox = document.getElementById('errorBox');
        const lastRefresh = document.getElementById('lastRefresh');
        const forceRefreshBtn = document.getElementById('forceRefreshBtn');
        const autoRefreshStatus = document.getElementById('autoRefreshStatus');
        const filterStatus = document.getElementById('filterStatus');
        const sortStatus = document.getElementById('sortStatus');
        const latestBeaconSignals = document.getElementById('latestBeaconSignals');
        const latestWifiSignals = document.getElementById('latestWifiSignals');
        const onlyAlertsCheckbox = document.getElementById('only_alerts');
        const autoScrollCheckbox = document.getElementById('auto_scroll_new');
        let timer = null;
        let previousRowIds = Array.from(document.querySelectorAll('[data-row-id]')).map(row => Number(row.dataset.rowId));

        function getRefreshSeconds() {
            const value = parseInt(document.getElementById('refresh_seconds').value, 10);
            if (Number.isNaN(value) || value < 0) {
                return 0;
            }
            return value;
        }

        function updateAutoRefreshLabel() {
            const seconds = getRefreshSeconds();
            autoRefreshStatus.textContent = seconds > 0 ? `Ogni ${seconds} secondi` : 'Disattivato';
        }

        function updateFilterLabel() {
            filterStatus.textContent = onlyAlertsCheckbox.checked ? 'Solo alert attivi' : 'Tutti i messaggi';
        }

        function updateSortLabel() {
            const sortBySelect = document.getElementById('sort_by');
            const sortDirSelect = document.getElementById('sort_dir');
            const sortByLabel = sortBySelect.options[sortBySelect.selectedIndex]?.text || 'ID';
            const sortDirLabel = (sortDirSelect.value || 'desc').toUpperCase();
            sortStatus.textContent = `${sortByLabel} · ${sortDirLabel}`;
        }

        function highlightNewRows(newIds) {
            if (!Array.isArray(newIds) || newIds.length === 0) {
                return;
            }

            const newRows = [];
            document.querySelectorAll('[data-row-id]').forEach((row) => {
                const rowId = Number(row.dataset.rowId);
                if (newIds.includes(rowId)) {
                    row.classList.add('new-row-flash');
                    newRows.push(row);
                    window.setTimeout(() => row.classList.remove('new-row-flash'), 2400);
                }
            });

            if (autoScrollCheckbox.checked && newRows.length > 0) {
                newRows[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
            }
        }

        function renderSignalList(items, emptyLabel) {
            if (!Array.isArray(items) || items.length === 0) {
                return `<div class="signal-empty">${emptyLabel}</div>`;
            }

            return `<div class="signal-list">${items.map((item) => {
                const mac = item?.mac ?? '';
                const rssi = item?.rssi ?? '—';
                return `<div class="signal-item"><span class="signal-mac mono">${escapeHtml(mac)}</span><span class="signal-rssi">RSSI ${escapeHtml(String(rssi))}</span></div>`;
            }).join('')}</div>`;
        }

        function updateSignalPanels(summary) {
            if (!latestBeaconSignals || !latestWifiSignals) {
                return;
            }

            latestBeaconSignals.innerHTML = renderSignalList(summary?.beacons ?? [], 'Nessun beacon nel messaggio più recente');
            latestWifiSignals.innerHTML = renderSignalList(summary?.wifi ?? [], 'Nessuna rete WiFi nel messaggio più recente');
        }

        function escapeHtml(value) {
            return String(value)
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#039;');
        }

        async function refreshData() {
            const imei = document.getElementById('imei').value.trim();
            const limit = document.getElementById('limit').value.trim();
            const refreshSeconds = document.getElementById('refresh_seconds').value.trim();
            const sortBy = document.getElementById('sort_by').value;
            const sortDir = document.getElementById('sort_dir').value;

            if (!imei || !limit) {
                return;
            }

            const params = new URLSearchParams({ imei, limit, refresh_seconds: refreshSeconds, sort_by: sortBy, sort_dir: sortDir, ajax: '1' });
            if (onlyAlertsCheckbox.checked) {
                params.set('only_alerts', '1');
            }

            results.classList.add('loading');
            forceRefreshBtn.disabled = true;

            try {
                const response = await fetch('?' + params.toString(), {
                    headers: { 'X-Requested-With': 'XMLHttpRequest' }
                });

                const data = await response.json();

                if (!data.ok) {
                    errorBox.textContent = data.error || 'Errore sconosciuto';
                    errorBox.style.display = 'block';
                    return;
                }

                errorBox.style.display = 'none';
                results.classList.remove('empty-state');
                results.innerHTML = data.html;
                lastRefresh.textContent = data.refreshed_at;
                updateFilterLabel();
                updateSortLabel();
                updateSignalPanels(data.signal_summary || {});

                const currentIds = Array.isArray(data.row_ids) ? data.row_ids.map(Number) : [];
                const newIds = currentIds.filter(id => !previousRowIds.includes(id));
                previousRowIds = currentIds;
                highlightNewRows(newIds);
            } catch (error) {
                errorBox.textContent = 'Errore durante il refresh: ' + error.message;
                errorBox.style.display = 'block';
            } finally {
                results.classList.remove('loading');
                forceRefreshBtn.disabled = false;
            }
        }

        function startAutoRefresh() {
            if (timer) {
                clearInterval(timer);
                timer = null;
            }

            const seconds = getRefreshSeconds();
            updateAutoRefreshLabel();
            updateFilterLabel();
            updateSortLabel();

            if (seconds > 0) {
                timer = setInterval(refreshData, seconds * 1000);
            }
        }

        document.addEventListener('click', function (event) {
            const button = event.target.closest('.toggle-details');
            if (!button) return;

            const id = button.getAttribute('data-target');
            const row = document.getElementById(id);
            if (!row) return;

            const isHidden = row.hasAttribute('hidden');
            if (isHidden) {
                row.removeAttribute('hidden');
                button.textContent = 'Chiudi';
            } else {
                row.setAttribute('hidden', 'hidden');
                button.textContent = 'Apri';
            }
        });

        form.addEventListener('submit', function () {
            previousRowIds = Array.from(document.querySelectorAll('[data-row-id]')).map(row => Number(row.dataset.rowId));
            startAutoRefresh();
        });

        document.getElementById('refresh_seconds').addEventListener('input', startAutoRefresh);
        document.getElementById('sort_by').addEventListener('change', function () { updateSortLabel(); refreshData(); startAutoRefresh(); });
        document.getElementById('sort_dir').addEventListener('change', function () { updateSortLabel(); refreshData(); startAutoRefresh(); });
        onlyAlertsCheckbox.addEventListener('change', function () {
            updateFilterLabel();
            refreshData();
            startAutoRefresh();
        });
        forceRefreshBtn.addEventListener('click', refreshData);

        if (document.getElementById('imei').value.trim()) {
            startAutoRefresh();
        } else {
            updateAutoRefreshLabel();
            updateFilterLabel();
            updateSortLabel();
        }
    </script>
</body>
</html>

