{include file='modules/servers/puqWebHosting/templates/header.tpl'}
{assign var='page_icon'  value='file-alt'}
{assign var='page_title' value=$L.ms_server_logs}
{include file='modules/servers/puqWebHosting/templates/_page_header.tpl'}

<style>
.puq-log-toolbar { display:flex; align-items:center; gap:8px; flex-wrap:wrap; padding:8px 12px; background:#fff; border:1px solid var(--puq-border); border-radius:var(--puq-radius); margin-bottom:10px; }
.puq-log-tabs { display:flex; gap:6px; flex-wrap:wrap; min-width:0; }
.puq-log-tab { padding:6px 12px; border:1px solid var(--puq-border); background:#f5f5f7; border-radius:4px; cursor:pointer; font-size:12px; line-height:1.3; white-space:nowrap; user-select:none; transition:background .12s ease; }
.puq-log-tab:hover { background:#e8e8eb; }
.puq-log-tab.active { background:var(--puq-primary); color:#fff; border-color:var(--puq-primary); }
.puq-log-tab.missing { opacity:.45; cursor:not-allowed; border-style:dashed; }
.puq-log-tab .puq-log-meta-inline { font-size:10px; opacity:.75; margin-left:4px; }
.puq-log-controls { display:flex; align-items:center; gap:6px; margin-left:auto; flex-shrink:0; }
.puq-log-controls .text-muted { font-size:11px; }
.puq-log-controls select { height:28px; font-size:11px; padding:0 6px; border-radius:3px; border:1px solid var(--puq-border); background:#fff; }
.puq-log-controls .puq-btn { padding:4px 8px; font-size:11px; }

/* Bulletproof containment for the log viewer pane.
   .puq-log-frame is the visible relative+overflow:hidden box.
   .puq-log-pre is position:absolute so its content (1500-char log lines)
   never contributes to ANY ancestor's intrinsic width. Internal
   overflow:auto gives horizontal+vertical scrollbars within the box. */
.puq-log-frame {
    position:relative; width:100%; max-width:100%; min-width:0;
    height:70vh; overflow:hidden; box-sizing:border-box;
    background:#1e1e1e; border-radius:var(--puq-radius);
}
.puq-log-pre {
    position:absolute; top:0; left:0; right:0; bottom:0;
    overflow:auto; margin:0; padding:10px; font-size:11.5px;
    background:transparent; color:#dcdcdc;
    white-space:pre; font-family:ui-monospace, 'JetBrains Mono', Menlo, Consolas, monospace;
}
.puq-log-pre .err  { color:#ff7b72; }
.puq-log-pre .warn { color:#f1c40f; }
.puq-log-pre .ok   { color:#7ee787; }
.puq-log-pre .dim  { color:#888; }
.puq-log-empty { color:#888; padding:40px; text-align:center; font-style:italic; }
.puq-log-rotated-mark { padding:2px 6px; background:#444; color:#dcdcdc; border-radius:3px; font-size:10px; margin:4px 0; display:inline-block; }
.puq-log-error-banner { color:#ff7b72; padding:8px 10px; background:#2b2b2b; border-bottom:1px solid #444; font-size:12px; }
.puq-log-rate-banner { color:#f1c40f; padding:8px 10px; background:#2b2b2b; border-bottom:1px solid #444; font-size:12px; }
.puq-log-status { font-size:11px; color:var(--puq-muted); display:flex; align-items:center; gap:6px; }
</style>

<div class="puq-msg puq-msg-info" style="font-size:13px;">
    <i class="fa fa-info-circle"></i>
    {$L.ms_logs_intro}
</div>

<div class="puq-log-toolbar">
    <div class="puq-log-tabs" id="puq-log-tabs">
        <span class="text-muted" style="font-size:12px;"><i class="fas fa-circle-notch fa-spin"></i> {$L.ms_loading_available_logs}</span>
    </div>
    <div class="puq-log-controls">
        <span class="puq-log-status" id="puq-log-status"></span>
        <label style="margin:0;font-weight:normal;font-size:11px;color:var(--puq-muted);">
            <input type="checkbox" id="puq-log-auto" checked> {$L.ms_auto}
        </label>
        <select id="puq-log-interval" title="{$L.ms_refresh_interval}">
            <option value="10000" selected>10s</option>
            <option value="20000">20s</option>
            <option value="30000">30s</option>
        </select>
        <button type="button" class="puq-btn puq-btn-outline puq-btn-sm" id="puq-log-refresh" title="{$L.ms_reload_now}"><i class="fa fa-redo"></i> {$L.ms_refresh}</button>
        <button type="button" class="puq-btn puq-btn-outline puq-btn-sm" id="puq-log-clear" title="{$L.ms_clear_viewer_hint}"><i class="fa fa-eraser"></i> {$L.ms_clear}</button>
        <button type="button" class="puq-btn puq-btn-primary puq-btn-sm" id="puq-log-download" title="{$L.ms_download_full_log}"><i class="fa fa-download"></i> {$L.ms_download}</button>
    </div>
</div>

<div class="puq-log-frame">
    <pre class="puq-log-pre" id="puq-log-content"><div class="puq-log-empty">{$L.ms_select_a_log_above}</div></pre>
</div>

<script>window.puqServiceId = {$service_id};</script>
{literal}
<script>
(function(){
    var esc = window.puqEsc;
    function fmtBytes(n){
        n = parseInt(n,10) || 0;
        if (n >= 1048576) return (n/1048576).toFixed(1) + ' MB';
        if (n >= 1024)    return (n/1024).toFixed(1) + ' KB';
        return n + ' B';
    }

    var state = {
        currentRole:  null,
        currentKey:   null,
        currentLabel: '',
        filterMode:   false,
        offset:       -1,
        size:         0,
        pollTimer:    null,
        autoActive:   true,
        rateLockedUntil: 0   // epoch ms — block client-side polls while we know the server will 429
    };

    function setStatus(text, kind){
        var $s = $('#puq-log-status');
        var color = kind === 'err'  ? 'var(--puq-danger)'
                  : kind === 'warn' ? 'var(--puq-warning)'
                  : kind === 'ok'   ? 'var(--puq-success)'
                  : 'var(--puq-muted)';
        $s.css('color', color).text(text || '');
    }

    function pillHtml(role, l){
        var cls = 'puq-log-tab';
        if (!l.exists || !l.readable) cls += ' missing';
        var title = (l.exists ? puqT('ms_log_available') : puqT('ms_log_not_present'))
                  + (l.exists && !l.readable ? ' ' + puqT('ms_log_not_readable') : '');
        var sz = l.exists ? fmtBytes(l.size || 0) : puqT('ms_na');
        return '<div class="' + cls + '" data-role="' + esc(role) + '" data-key="' + esc(l.key) + '" '
            +    'data-filter="' + (l.filter_domain ? '1' : '0') + '" title="' + esc(title) + '">'
            +    esc(l.label) + ' <span class="puq-log-meta-inline">' + esc(sz) + '</span>'
            + '</div>';
    }

    function renderCatalog(d){
        var html = '';
        if (d.web && d.web.logs && d.web.logs.length) {
            d.web.logs.forEach(function(l){ html += pillHtml('web', l); });
        }
        if (d.mail && d.mail.logs && d.mail.logs.length) {
            d.mail.logs.forEach(function(l){ html += pillHtml('mail', l); });
        }
        if (html === '') {
            html = '<span class="text-muted" style="font-size:12px;">' + esc(puqT('ms_no_logs_available')) + '</span>';
        }
        $('#puq-log-tabs').html(html);

        if (!state.currentKey) {
            var $first = $('#puq-log-tabs .puq-log-tab').not('.missing').first();
            if ($first.length) selectLog($first);
        }
    }

    function loadCatalog(){
        puqClientAjax('logsCatalog', {}, function(r){
            if (!r || r.status !== 'success' || !r.data) {
                var msg = (r && r.msg) || puqT('ms_failed_load_catalog');
                if (r && r.code === 429) {
                    state.rateLockedUntil = Date.now() + (r.retry_after || 10) * 1000;
                    $('#puq-log-tabs').html('<span class="text-warning" style="font-size:12px;">' + esc(msg) + '</span>');
                } else {
                    $('#puq-log-tabs').html('<span class="text-danger" style="font-size:12px;">' + esc(msg) + '</span>');
                }
                return;
            }
            renderCatalog(r.data);
        });
    }

    function colorize(line){
        var l = (line || '').toString();
        if (/\[error\]|\bERROR\b|\bFATAL\b|" 5\d\d /.test(l)) return '<span class="err">'  + esc(l) + '</span>';
        if (/\[warn\]|\bWARNING\b|" 4\d\d /.test(l))          return '<span class="warn">' + esc(l) + '</span>';
        if (/" 2\d\d /.test(l))                                return '<span class="ok">'   + esc(l) + '</span>';
        return esc(l);
    }

    function appendToView(chunk, rotated){
        var $pre = $('#puq-log-content');
        var nearBottom = ($pre[0].scrollTop + $pre[0].clientHeight + 24) >= $pre[0].scrollHeight;
        if ($pre.find('.puq-log-empty').length) $pre.empty();
        if (rotated) $pre.append('<div class="puq-log-rotated-mark">↻ ' + esc(puqT('ms_log_rotated')) + '</div>');
        if (!chunk) return;
        var html = chunk.split('\n').map(colorize).join('\n');
        $pre.append(html);
        if (nearBottom) $pre.scrollTop($pre[0].scrollHeight);
        // Cap rendered text length so the viewer doesn't bloat the page.
        var txtLen = $pre[0].textContent.length;
        if (txtLen > 400000) {
            $pre[0].innerHTML = $pre[0].innerHTML.substring($pre[0].innerHTML.length - 380000);
        }
    }

    function replaceView(chunk){
        var $pre = $('#puq-log-content');
        $pre.empty();
        if (!chunk) {
            $pre.html('<div class="puq-log-empty">' + esc(puqT('ms_no_matching_lines')) + '</div>');
            return;
        }
        var html = chunk.split('\n').map(colorize).join('\n');
        $pre.html(html);
        $pre.scrollTop($pre[0].scrollHeight);
    }

    function poll(){
        if (!state.currentKey) return;
        // Client-side rate guard — if we just saw a 429, don't even attempt
        // another request until that window has passed.
        if (Date.now() < state.rateLockedUntil) {
            var wait = Math.ceil((state.rateLockedUntil - Date.now()) / 1000);
            setStatus(puqT('ms_rate_limited') + ' · ' + puqT('ms_retry_in') + ' ' + wait + 's', 'warn');
            return;
        }
        setStatus(puqT('ms_fetching'));
        var data = {
            role:       state.currentRole,
            key:        state.currentKey,
            offset:     state.filterMode ? 0 : state.offset,
            max_bytes:  state.filterMode ? 131072 : 32768,
            tail_lines: 500
        };
        puqClientAjax('logTail', data, function(r){
            if (!r || r.status !== 'success') {
                if (r && r.code === 429) {
                    state.rateLockedUntil = Date.now() + (r.retry_after || 5) * 1000;
                    setStatus(puqT('ms_rate_limited') + ' · ' + puqT('ms_retry_in') + ' ' + (r.retry_after || 5) + 's', 'warn');
                    $('#puq-log-content').prepend('<div class="puq-log-rate-banner">' + esc(r.msg) + '</div>');
                    return;
                }
                setStatus((r && r.msg) || puqT('ms_request_failed'), 'err');
                $('#puq-log-content').html('<div class="puq-log-error-banner">' + esc((r && r.msg) || puqT('ms_tail_failed')) + '</div>');
                state.autoActive = false;
                $('#puq-log-auto').prop('checked', false);
                stopPoll();
                return;
            }
            var d = r.data;
            setStatus(puqT('ms_size') + ' ' + fmtBytes(d.size) + (d.mode === 'filtered' ? ' · ' + puqT('ms_filtered') : ' · ' + puqT('ms_offset') + ' ' + d.new_offset), 'ok');
            if (state.filterMode) {
                replaceView(d.content);
            } else {
                appendToView(d.content, !!d.rotated);
                state.offset = d.new_offset;
            }
            state.size = d.size;
        }, 'POST');
    }

    function stopPoll(){ if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; } }
    function startPoll(){
        stopPoll();
        if (!state.autoActive) return;
        var ms = parseInt($('#puq-log-interval').val(), 10) || 10000;
        state.pollTimer = setInterval(poll, ms);
    }

    function selectLog($el){
        if ($el.hasClass('missing')) return;
        $('#puq-log-tabs .puq-log-tab').removeClass('active');
        $el.addClass('active');
        state.currentRole = $el.data('role');
        state.currentKey  = $el.data('key');
        state.filterMode  = $el.data('filter') === 1 || $el.data('filter') === '1';
        state.offset      = -1;
        state.currentLabel = $el.text().trim();
        $('#puq-log-content').html('<div class="puq-log-empty"><i class="fas fa-circle-notch fa-spin"></i> ' + esc(puqT('ms_reading')) + '</div>');
        poll();
        startPoll();
    }

    $('#puq-log-tabs').on('click', '.puq-log-tab', function(){
        selectLog($(this));
    });

    $('#puq-log-refresh').on('click', function(e){
        e.preventDefault();
        if (!state.currentKey) { loadCatalog(); return; }
        state.offset = -1;
        $('#puq-log-content').html('<div class="puq-log-empty"><i class="fas fa-circle-notch fa-spin"></i> ' + esc(puqT('ms_refreshing')) + '</div>');
        poll();
    });

    $('#puq-log-clear').on('click', function(e){
        e.preventDefault();
        $('#puq-log-content').html('<div class="puq-log-empty">' + esc(puqT('ms_view_cleared')) + '</div>');
    });

    $('#puq-log-download').on('click', function(e){
        e.preventDefault();
        if (!state.currentKey) { puqToast('info', puqT('ms_select_log_first')); return; }
        // Server-side rate-limit applies to logDownload too; open in a new
        // tab so a 429 (or any other error) is shown as JSON instead of
        // breaking the current page.
        var u = 'clientarea.php?action=productdetails&id=' + window.puqServiceId
              + '&ajax=logDownload'
              + '&role=' + encodeURIComponent(state.currentRole)
              + '&key='  + encodeURIComponent(state.currentKey);
        window.open(u, '_blank', 'noopener,noreferrer');
    });

    $('#puq-log-auto').on('change', function(){
        state.autoActive = this.checked;
        if (state.autoActive) startPoll(); else stopPoll();
    });
    $('#puq-log-interval').on('change', function(){
        if (state.autoActive) startPoll();
    });

    loadCatalog();
})();
</script>
{/literal}
