Logging Handler & UI Overlay

Logger that optionally mirror to a on-page UI overlay shared by every page match-common userscript

Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greasyfork.org/scripts/588114/1882370/Logging%20Handler%20%20UI%20Overlay.js

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name           Logging Handler & UI Overlay
// @namespace      861ddd094884eac5bea7a3b12e074f34
// @description    Logger that optionally mirror to a on-page UI overlay shared by every page match-common userscript
// @author         Anonymous, Claude Opus 4.8
// @version        1.0.0
// @license        BSD-0
// @grant          none
// ==/UserScript==

(function () {
    'use strict';

    const PANEL_ID = 'logui-panel';
    const STYLE_ID = 'logui-style';
    const BODY_CLASS = 'logui-body';
    const VISIBLE_ATTR = 'data-logui-visible';
    const DEFAULT_MAX_LINES = 500;
    const DEFAULT_PREFS_KEY = 'logui_prefs';
    const LEVELS = ['debug', 'info', 'warn', 'error'];

    const CSS = `
#${PANEL_ID} {
    position: fixed;
    top: 20px;
    right: 40px;
    width: 420px;
    max-width: 22.5vw;
    height: 20vh;
    z-index: 2147483647;
    display: flex;
    flex-direction: column;
    background: rgba(0, 0, 0, 0.10);
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 6px;
    font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    /* Click-through, so the panel never steals input from the page beneath. */
    pointer-events: none;
    overflow-y: auto;
    scrollbar-width: none;
}
#${PANEL_ID}::-webkit-scrollbar { display: none; }
#${PANEL_ID}:hover { background: rgba(0, 0, 0, 0.30); }
#${PANEL_ID} .${BODY_CLASS} {
    flex: 1 1 auto;
    min-height: 0;
    padding: 6px 8px;
    word-break: break-word;
    overflow-y: auto;
    scrollbar-width: none;
    /* Dimmed until hovered: legible on demand, unobtrusive otherwise. Opacity
       lives on the body alone so per-level colours stay a single flat value.
       Hover here still satisfies the :hover rule above via propagation, and
       re-enables events so the log can be scrolled. */
    opacity: 0.30;
    pointer-events: auto;
    transition: opacity 120ms ease;
}
#${PANEL_ID} .${BODY_CLASS}:hover { opacity: 1; }
#${PANEL_ID} .logui-line {
    margin: 0 0 3px;
    white-space: pre-wrap;
}
#${PANEL_ID} .logui-time { color: #9aa0a6; margin-right: 6px; }
#${PANEL_ID} .logui-tag { margin-right: 6px; font-weight: 600; }
#${PANEL_ID} .logui-debug { color: #e6e6e6; }
#${PANEL_ID} .logui-info  { color: #6fb3ff; }
#${PANEL_ID} .logui-warn  { color: #ffc857; }
#${PANEL_ID} .logui-error { color: #ff6b6b; }
`;

    // storage
    ///////////

    // The calling script owns the grants, so probe rather than assume. A script
    // that granted neither still gets a working logger, just a forgetful one.
    const hasGet = typeof GM_getValue === 'function';
    const hasSet = typeof GM_setValue === 'function';

    function readPrefs(key) {
        if (!hasGet) return {};
        try {
            const raw = GM_getValue(key, null);
            if (!raw) return {};
            const parsed = JSON.parse(raw);
            return parsed && typeof parsed === 'object' ? parsed : {};
        } catch (e) {
            return {};
        }
    }

    function writePrefs(key, value) {
        if (!hasSet) return false;
        try {
            GM_setValue(key, JSON.stringify(value));
            return true;
        } catch (e) {
            return false;
        }
    }

    // panel
    /////////

    // Whichever instance logs first builds the panel; the rest adopt it. Both
    // lookups are by document id, which is the only channel sandboxed scripts
    // reliably share.
    function ensurePanel() {
        const host = document.body || document.documentElement;
        if (!host) return null;

        let panel = document.getElementById(PANEL_ID);
        if (panel) return panel;

        if (!document.getElementById(STYLE_ID)) {
            const style = document.createElement('style');
            style.id = STYLE_ID;
            style.textContent = CSS;
            (document.head || host).appendChild(style);
        }

        panel = document.createElement('div');
        panel.id = PANEL_ID;
        const body = document.createElement('div');
        body.className = BODY_CLASS;
        panel.appendChild(body);
        host.appendChild(panel);
        return panel;
    }

    function panelBody(panel) {
        return panel ? panel.querySelector('.' + BODY_CLASS) : null;
    }

    // Deterministic hue per tag: the same tag is the same colour in every
    // script and on every page, with no shared allocation table. Saturation and
    // lightness are fixed high enough to stay readable on the dark panel.
    function tagColour(tag) {
        let hash = 0;
        for (let i = 0; i < tag.length; i++) {
            hash = (hash * 31 + tag.charCodeAt(i)) >>> 0;
        }
        return `hsl(${hash % 360}, 65%, 70%)`;
    }

    function formatArg(value) {
        if (typeof value === 'string') return value;
        if (value instanceof Error) return value.stack || String(value);
        try {
            return JSON.stringify(value);
        } catch (e) {
            return String(value);
        }
    }

    // instances
    /////////////

    function create(config) {
        const cfg = config || {};
        const tag = String(cfg.tag || cfg.name || 'log');
        const consolePrefix = cfg.name ? `[${cfg.name}] ` : `[${tag}] `;
        const prefsKey = cfg.prefsKey || DEFAULT_PREFS_KEY;
        const maxLines = cfg.maxLines > 0 ? cfg.maxLines : DEFAULT_MAX_LINES;
        // A surface where the panel makes no sense (a secondary @match, say) can
        // opt this instance out of the panel sink while keeping the console one.
        const panelSink = cfg.panelSink !== false;
        const minLevel = LEVELS.indexOf(cfg.minLevel) >= 0
            ? LEVELS.indexOf(cfg.minLevel) : 0;
        const colour = tagColour(tag);

        const stored = readPrefs(prefsKey);
        let consoleOn = typeof stored.console === 'boolean'
            ? stored.console
            : cfg.console !== false;
        let panelOn = typeof stored.panel === 'boolean'
            ? stored.panel
            : cfg.panel !== false;

        function persist() {
            writePrefs(prefsKey, { console: consoleOn, panel: panelOn });
        }

        // Push this instance's notion of visibility onto the shared node, and
        // stamp it so sibling instances can follow.
        function applyPanel(build) {
            const panel = build ? ensurePanel() : document.getElementById(PANEL_ID);
            if (!panel) return null;
            panel.style.display = panelOn ? 'flex' : 'none';
            if (panel.getAttribute(VISIBLE_ATTR) !== String(panelOn)) {
                panel.setAttribute(VISIBLE_ATTR, String(panelOn));
            }
            return panel;
        }

        // The converse: adopt a sibling's flip. Watching the attribute rather
        // than the style keeps this from firing on our own display writes.
        function watchPanel(panel) {
            if (!panel || !window.MutationObserver) return;
            new MutationObserver(() => {
                const shared = panel.getAttribute(VISIBLE_ATTR) === 'true';
                if (shared === panelOn) return;
                panelOn = shared;
                panel.style.display = panelOn ? 'flex' : 'none';
                persist();
            }).observe(panel, { attributes: true, attributeFilter: [VISIBLE_ATTR] });
        }

        function appendLine(level, args) {
            const panel = applyPanel(true);
            const body = panelBody(panel);
            if (!body) return;

            // Only chase the newest line when the reader is already parked at
            // the bottom; otherwise leave a manual scroll-up undisturbed.
            const atBottom =
                body.scrollHeight - body.scrollTop - body.clientHeight < 4;

            const line = document.createElement('div');
            line.className = 'logui-line logui-' + level;

            const time = document.createElement('span');
            time.className = 'logui-time';
            time.textContent = new Date().toLocaleTimeString();
            line.appendChild(time);

            const label = document.createElement('span');
            label.className = 'logui-tag';
            label.style.color = colour;
            label.textContent = tag;
            line.appendChild(label);

            line.appendChild(document.createTextNode(args.map(formatArg).join(' ')));
            body.appendChild(line);

            // Newest lines settle at the bottom; older ones are trimmed so a
            // long-lived session cannot grow the node list without bound.
            while (body.childElementCount > maxLines) {
                body.removeChild(body.firstChild);
            }
            if (atBottom) body.scrollTop = body.scrollHeight;
        }

        function emit(level, args) {
            if (LEVELS.indexOf(level) < minLevel) return;
            if (consoleOn) {
                // Objects are handed to the console unstringified, so devtools
                // keeps them inspectable; only the panel flattens them.
                const sink = console[level] || console.log;
                sink.call(console, consolePrefix, ...args);
            }
            if (panelOn && panelSink) appendLine(level, args);
        }

        const logger = {
            debug: (...args) => emit('debug', args),
            info: (...args) => emit('info', args),
            warn: (...args) => emit('warn', args),
            error: (...args) => emit('error', args),
            clear() {
                const body = panelBody(document.getElementById(PANEL_ID));
                if (body) body.textContent = '';
            },
            toggleConsole() {
                consoleOn = !consoleOn;
                persist();
                return consoleOn;
            },
            togglePanel() {
                panelOn = !panelOn;
                persist();
                applyPanel(panelOn);
                return panelOn;
            },
            get consoleEnabled() { return consoleOn; },
            set consoleEnabled(on) {
                consoleOn = !!on;
                persist();
            },
            get panelEnabled() { return panelOn; },
            set panelEnabled(on) {
                panelOn = !!on;
                persist();
                applyPanel(panelOn);
            },
            get element() { return document.getElementById(PANEL_ID); },
            // Convenience wiring for the two toggles, skipped silently when the
            // caller did not grant GM_registerMenuCommand.
            registerMenuCommands(opts) {
                if (typeof GM_registerMenuCommand !== 'function') return false;
                const o = opts || {};
                GM_registerMenuCommand(o.consoleLabel || 'Toggle console logging', () => {
                    const on = logger.toggleConsole();
                    // Announce through whichever sink is still live.
                    logger.info(`console logging ${on ? 'enabled' : 'disabled'}`);
                });
                GM_registerMenuCommand(o.panelLabel || 'Toggle log panel', () => {
                    const on = logger.togglePanel();
                    logger.info(`log panel ${on ? 'enabled' : 'disabled'}`);
                });
                return true;
            },
        };
        logger.log = logger.info;

        // Build eagerly -- hidden if this instance's preference says so -- so
        // there is always a node to watch for sibling flips, and so a later
        // toggle has nothing to construct.
        if (panelSink) {
            const panel = applyPanel(true);
            if (panel) {
                watchPanel(panel);
            } else {
                document.addEventListener('DOMContentLoaded', () => {
                    watchPanel(applyPanel(true));
                }, { once: true });
            }
        }

        return logger;
    }

    window.LoggingUI = { create, LEVELS };
})();