Plugin Directory

Changeset 3793353


Ignore:
Timestamp:
11/09/2026 10:20:28 PM (41 hours ago)
Author:
businesskiai
Message:

Release 1.0.12

  • Loading time: set a starting point after optimising the website – from then on only new measurements count, earlier values stay visible for comparison.
  • Broken links: new "AI briefing" – a ready-made text for an AI chat that guides you step by step through repairing the links.
Location:
smartengin-analytics/trunk
Files:
1 added
10 edited

Legend:

Unmodified
Added
Removed
  • smartengin-analytics/trunk/assets/css/admin.css

    r3791304 r3793353  
    425425/* 1.0.11: filter dropdowns never wider than the screen (long result names on phones). */
    426426.sean-filters select { max-width: 100%; }
     427
     428/* Loading time: starting point after an optimisation (1.0.12). */
     429.sean-cwv-before { display: block; font-size: 12px; margin-top: 2px; }
     430.sean-vitals-start { margin-top: 16px; padding: 12px 14px; border: 1px solid var(--sean-border); border-radius: 6px; background: #f8fafb; }
     431.sean-vitals-start p { margin: 0 0 8px; }
     432.sean-vitals-start p:last-child { margin-bottom: 0; }
     433.sean-vitals-start--set { margin: 0 0 14px; border-color: #b9d8c6; background: #f3faf6; }
     434.sean-vitals-start input[type="date"] { margin-left: 4px; }
     435.sean-vitals-start__done { color: #1e8a4f; font-weight: 600; margin: 0 0 8px; }
     436
     437/* AI briefing for the broken links (1.0.12). */
     438.sean-brief { margin-top: 14px; padding: 10px 14px; border: 1px solid #c9d6e2; border-radius: 6px; background: #f6f9fc; }
     439.sean-brief summary { cursor: pointer; font-weight: 600; color: #11314a; }
     440.sean-brief__buttons .button { margin: 0 6px 6px 0; }
     441.sean-brief__text { width: 100%; font-size: 12px; line-height: 1.5; min-height: 260px; }
  • smartengin-analytics/trunk/assets/js/admin-tech.js

    r3744444 r3793353  
    7676        }
    7777})();
     78
     79/* AI briefing for the broken links (1.0.12): built on request, then copy or save as a Markdown file. */
     80(function () {
     81        'use strict';
     82        var btn = document.querySelector('[data-sean-brief]');
     83        if (!btn || !window.fetch) { return; }
     84        var cfg;
     85        try { cfg = JSON.parse(btn.getAttribute('data-sean-brief')); } catch (e) { return; }
     86        var box = document.getElementById('sean-brief-text');
     87        var copy = document.getElementById('sean-brief-copy');
     88        var save = document.getElementById('sean-brief-save');
     89        var status = document.getElementById('sean-brief-status');
     90        function say(t) { if (status) { status.textContent = t; } }
     91
     92        btn.addEventListener('click', function () {
     93                btn.disabled = true;
     94                say(cfg.busy);
     95                var body = new URLSearchParams();
     96                body.append('action', 'sean_link_brief');
     97                body.append('_ajax_nonce', cfg.nonce);
     98                fetch(cfg.url, { method: 'POST', credentials: 'same-origin', body: body })
     99                        .then(function (r) { return r.json(); })
     100                        .then(function (res) {
     101                                if (!res || !res.success || !res.data || typeof res.data.text !== 'string') { throw new Error('brief'); }
     102                                box.value = res.data.text;
     103                                box.hidden = false;
     104                                copy.hidden = false;
     105                                save.hidden = false;
     106                                btn.disabled = false;
     107                                say(cfg.ready);
     108                        })
     109                        .catch(function () { btn.disabled = false; say(cfg.failed); });
     110        });
     111
     112        if (copy) {
     113                copy.addEventListener('click', function () {
     114                        function done() { say(cfg.copied); }
     115                        if (navigator.clipboard && navigator.clipboard.writeText) {
     116                                navigator.clipboard.writeText(box.value).then(done, function () { box.select(); try { document.execCommand('copy'); } catch (e) {} done(); });
     117                        } else {
     118                                box.select();
     119                                try { document.execCommand('copy'); } catch (e) {}
     120                                done();
     121                        }
     122                });
     123        }
     124        if (save) {
     125                save.addEventListener('click', function () {
     126                        var blob = new Blob([box.value], { type: 'text/markdown;charset=utf-8' });
     127                        var a = document.createElement('a');
     128                        a.href = URL.createObjectURL(blob);
     129                        a.download = cfg.file;
     130                        document.body.appendChild(a);
     131                        a.click();
     132                        setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
     133                });
     134        }
     135})();
  • smartengin-analytics/trunk/help/en.php

    r3791304 r3793353  
    8989                '<p>The tab <a href="{technical}">Technical</a> shows what needs care on the website. Choose the period at the top, as in the dashboard.</p>
    9090                <ul>
    91                         <li><strong>Broken links</strong> – links in your posts, pages and menus – with a block theme also in the site editor’s templates (the front page, for example), header and footer, patterns and navigation menus – that lead nowhere, with where they are, “Edit” and “Show on page” (opens the page exactly at this link, highlighted – handy when a page has several similar buttons; very old browsers just open the page). Buttons of blocks that build their content only when the page is shown (hero, tile, banner or the header button of a block plugin, for example) are found too. <strong>Elementor:</strong> pages and Elementor templates are read the way Elementor shows them to visitors – buttons, headings, icon lists, text fields, image links, embedded templates and shortcodes. “Edit” then opens the Elementor editor. With <strong>Elementor Pro</strong> also headers and footers from the Theme Builder, popups, global widgets, the “Template” widget, Pro widgets like Call to Action or Price Table, and dynamic links (“Post URL”, for example). Headers, footers and other Theme Builder templates without display conditions are seen by no visitor – their links are not checked; as soon as you set a condition, they are included. <strong>WooCommerce:</strong> products are checked with their description and short description, external (affiliate) products also with the address of their buy button – it often breaks when the other shop removes the product. Plus the descriptions of product categories and product tags. “Edit” opens the product or category. Note: with a block theme, WooCommerce’s default product template shows the short description as plain text – a link in it cannot be clicked there, but it is reported anyway (it is in the short description and shows as soon as the theme or template displays it). Customer reviews and comments are not checked. A jump mark without a page address (just “#contact”) in an Elementor template is not checked, as the template can be on different pages. Checked in the background, one portion an hour; every link again after a week, broken ones after a day. When many links are due (the first time, for example), the next portion follows every minute until all are checked. “Check now” collects all links of the website afresh (including one changed a minute ago) and checks every one right away while the page stays open, with a progress display. Links to a jump mark (for example …/#contact) are checked on the target page: if there is no element with that name (in Elementor: “CSS ID” or the widget “Menu Anchor”), the link counts as broken – it only opens the top of the page. A jump mark without a page address (just “#contact”) in the header or footer is on every page – which one is meant is unknown, so such links are not checked. The line under the heading is green (all fine), orange (unsure or not all checked yet) or red (broken links).
     91                        <li><strong>Broken links</strong> – links in your posts, pages and menus – with a block theme also in the site editor’s templates (the front page, for example), header and footer, patterns and navigation menus – that lead nowhere, with where they are, “Edit” and “Show on page” (opens the page exactly at this link, highlighted – handy when a page has several similar buttons; very old browsers just open the page). Buttons of blocks that build their content only when the page is shown (hero, tile, banner or the header button of a block plugin, for example) are found too. <strong>Elementor:</strong> pages and Elementor templates are read the way Elementor shows them to visitors – buttons, headings, icon lists, text fields, image links, embedded templates and shortcodes. “Edit” then opens the Elementor editor. With <strong>Elementor Pro</strong> also headers and footers from the Theme Builder, popups, global widgets, the “Template” widget, Pro widgets like Call to Action or Price Table, and dynamic links (“Post URL”, for example). Headers, footers and other Theme Builder templates without display conditions are seen by no visitor – their links are not checked; as soon as you set a condition, they are included. <strong>WooCommerce:</strong> products are checked with their description and short description, external (affiliate) products also with the address of their buy button – it often breaks when the other shop removes the product. Plus the descriptions of product categories and product tags. “Edit” opens the product or category. Note: with a block theme, WooCommerce’s default product template shows the short description as plain text – a link in it cannot be clicked there, but it is reported anyway (it is in the short description and shows as soon as the theme or template displays it). Customer reviews and comments are not checked. A jump mark without a page address (just “#contact”) in an Elementor template is not checked, as the template can be on different pages. Checked in the background, one portion an hour; every link again after a week, broken ones after a day. When many links are due (the first time, for example), the next portion follows every minute until all are checked. “Check now” collects all links of the website afresh (including one changed a minute ago) and checks every one right away while the page stays open, with a progress display. Links to a jump mark (for example …/#contact) are checked on the target page: if there is no element with that name (in Elementor: “CSS ID” or the widget “Menu Anchor”), the link counts as broken – it only opens the top of the page. A jump mark without a page address (just “#contact”) in the header or footer is on every page – which one is meant is unknown, so such links are not checked. The line under the heading is green (all fine), orange (unsure or not all checked yet) or red (broken links). <strong>AI briefing:</strong> under the list, “AI briefing: repair the links together with an AI” → “Create briefing” makes a text for ChatGPT, Claude, Gemini or another AI chat – the task, how to guide you step by step, what each problem means, your website (WordPress, theme, page builder), every broken link with where it is and its “Edit” link, the jump marks that really exist on the target pages and your published pages as possible targets. “Copy text” or “Save as file”, paste it into the AI chat, and the AI walks you through the repairs one link at a time. Nothing is sent anywhere until you paste it yourself; the text contains addresses of your website, no visitor data.
    9292                                <ul>
    9393                                        <li>Links to your own pages are checked in the database where possible (page published? file there?). A page that is a draft, private or in the trash makes the link broken.</li>
    … …  
    104104                                </ul>
    105105                                Google judges each value by the <strong>75th percentile</strong>: at least 3 of 4 page views must be in the green range. The number shown is exactly that value – three quarters of the visits were this fast or faster. The bar below shows the shares good / needs improvement / poor. “Passed” means LCP, INP and CLS are all good. A verdict needs at least 20 measurements; before that the value is already shown in its colour, but marked “provisional” – it can still change a lot with the next visits.
     106                                <strong>After optimising the website:</strong> under the values, “Website optimised?” sets a starting point (today – then only page views from this moment – or the day after your work). From then on only new measurements count: in this card, in “Loading time per page” (Pro), in the weekly and monthly report and in the alerts. Nothing is deleted – under each value “before” shows the 28 days before the starting point, so you see what the work brought. Empty the page cache first, so visitors get the optimised pages. “Remove starting point” makes all measurements count again.
    106107                                <br>Google’s own numbers (Search Console, PageSpeed Insights) come from Chrome users only, over 28 days, and can therefore differ. Measured at the visitor, with the reports the tracker sends anyway – no extra request, nothing stored in the browser. A page view counts once the visitor stays 10 seconds or leaves the page. Switch off: <a href="{settings}">Settings → Counting → Measure loading time</a>. With <strong>smartEngin Analytics Pro</strong> a list per page follows below the card.</li>
    107108                        <li><strong>Pages not found (404)</strong> – addresses visitors opened that do not exist: old links, typos in a newsletter, renamed pages. Below them the websites linking to such addresses. Fix the link or set up a redirect (for example with the plugin Redirection). Probes for vulnerable files (/.env and the like) are not here but in the Security tab.</li>
  • smartengin-analytics/trunk/includes/admin/class-sean-tech-tab.php

    r3744444 r3793353  
    2525                add_action( 'admin_post_sean_link_check', array( __CLASS__, 'check_now' ) );
    2626                add_action( 'wp_ajax_sean_link_check_step', array( __CLASS__, 'ajax_step' ) );
     27                add_action( 'admin_post_sean_vitals_start', array( __CLASS__, 'vitals_start' ) );
     28        }
     29
     30        /**
     31         * Set or remove the starting point of the loading time (admin-post, card "Loading time").
     32         *
     33         * @return void
     34         */
     35        public static function vitals_start() {
     36                if ( ! current_user_can( SEAN_Admin::capability() ) ) {
     37                        wp_die( esc_html__( 'You do not have permission to see this page.', 'smartengin-analytics' ) );
     38                }
     39                check_admin_referer( 'sean_vitals_start' );
     40                if ( ! empty( $_POST['sean_remove'] ) ) {
     41                        SEAN_Vitals::clear_start();
     42                        $msg = 'vitals_start_removed';
     43                } else {
     44                        $today = wp_date( 'Y-m-d' );
     45                        $day   = isset( $_POST['sean_start_day'] ) ? sanitize_text_field( wp_unslash( $_POST['sean_start_day'] ) ) : '';
     46                        if ( ! preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $day, $m ) || ! checkdate( (int) $m[2], (int) $m[3], (int) $m[1] ) || $day > $today || $day < '2020-01-01' ) {
     47                                $day = $today;
     48                        }
     49                        // Today: from this moment (the measurements of the morning still show the old website);
     50                        // an earlier day: from its start.
     51                        $ts = $day === $today ? time() : ( new DateTimeImmutable( $day . ' 00:00:00', wp_timezone() ) )->getTimestamp();
     52                        SEAN_Vitals::set_start( $ts );
     53                        $msg = 'vitals_start';
     54                }
     55                // Back to the same period.
     56                $args = array( 'sean_msg' => $msg );
     57                foreach ( array( 'range', 'from', 'to', 'back' ) as $k ) {
     58                        if ( isset( $_POST[ 'sean_r_' . $k ] ) ) {
     59                                $args[ $k ] = sanitize_text_field( wp_unslash( $_POST[ 'sean_r_' . $k ] ) );
     60                        }
     61                }
     62                wp_safe_redirect( SEAN_Admin::url( 'technical', $args ) . '#sean-vitals' );
     63                exit;
    2764        }
    2865
    … …  
    190227                } else {
    191228                        self::problem_table( $p );
     229                        self::brief_box();
    192230                }
    193231                // Links that could not be checked (Pierre 10.10.2026: fold-out with a very discreet orange note).
    … …  
    201239                }
    202240                echo '</div>';
     241        }
     242
     243        /**
     244         * "AI briefing": a text to copy into an AI chat that then guides through the repairs (1.0.12).
     245         *
     246         * @return void
     247         */
     248        private static function brief_box() {
     249                $cfg = array(
     250                        'url'    => admin_url( 'admin-ajax.php' ),
     251                        'nonce'  => wp_create_nonce( 'sean_link_brief' ),
     252                        'busy'   => __( 'Creating the briefing … (the pages with broken jump marks are read, this takes a few seconds)', 'smartengin-analytics' ),
     253                        'ready'  => __( 'Ready. Copy the text and paste it into ChatGPT, Claude, Gemini or another AI chat.', 'smartengin-analytics' ),
     254                        'failed' => __( 'The briefing could not be created. Try again in a moment.', 'smartengin-analytics' ),
     255                        'copied' => __( 'Copied – now paste it into the AI chat.', 'smartengin-analytics' ),
     256                        'file'   => 'ki-briefing-kaputte-links-' . wp_date( 'Y-m-d' ) . '.md',
     257                );
     258                echo '<details class="sean-brief" id="sean-brief"><summary>' . esc_html__( 'AI briefing: repair the links together with an AI', 'smartengin-analytics' ) . '</summary>';
     259                echo '<p class="sean-hint">' . esc_html__( 'Creates a text for an AI chat (ChatGPT, Claude, Gemini …): the task, how to guide you step by step, what each problem means, your website (WordPress, theme, page builder), every broken link with where it is and the “Edit” link, the jump marks that exist on the target pages and your published pages as possible targets. The AI then walks you through the repairs one link at a time. The text contains addresses of your website, no visitor data – nothing is sent anywhere until you paste it yourself.', 'smartengin-analytics' ) . '</p>';
     260                echo '<p class="sean-brief__buttons"><button type="button" class="button button-primary" data-sean-brief="' . esc_attr( wp_json_encode( $cfg ) ) . '">' . esc_html__( 'Create briefing', 'smartengin-analytics' ) . '</button> ';
     261                echo '<button type="button" class="button" id="sean-brief-copy" hidden>' . esc_html__( 'Copy text', 'smartengin-analytics' ) . '</button> ';
     262                echo '<button type="button" class="button" id="sean-brief-save" hidden>' . esc_html__( 'Save as file', 'smartengin-analytics' ) . '</button> ';
     263                echo '<span class="sean-hint" id="sean-brief-status" aria-live="polite"></span></p>';
     264                echo '<textarea id="sean-brief-text" class="large-text code sean-brief__text" rows="18" readonly hidden aria-label="' . esc_attr__( 'AI briefing', 'smartengin-analytics' ) . '"></textarea>';
     265                echo '</details>';
    203266        }
    204267
  • smartengin-analytics/trunk/includes/class-sean-extras.php

    r3744444 r3793353  
    103103                        return;
    104104                }
    105                 $sum   = SEAN_Vitals::summary( $r['from'], $r['to'] );
     105                $sum   = SEAN_Vitals::summary( $r['from'], $r['to'] ); // From the starting point on, when there is one.
     106                $sday  = SEAN_Vitals::start_day();
     107                $reach = '' !== $sday && $r['to'] >= $sday;
     108                // "Before": the 28 days before the starting point, as they were (1.0.12).
     109                $before = null;
     110                if ( $reach ) {
     111                        $before = SEAN_Vitals::summary( gmdate( 'Y-m-d', (int) strtotime( $sday . ' -28 days' ) ), gmdate( 'Y-m-d', (int) strtotime( $sday . ' -1 day' ) ), false );
     112                        if ( SEAN_Vitals::count_all( $before ) < 1 ) {
     113                                $before = null;
     114                        }
     115                }
    106116                $devs  = array(
    107117                        'm' => __( 'Phone & tablet', 'smartengin-analytics' ),
    … …  
    125135                        echo '<p class="sean-hint">' . esc_html__( 'Measuring the loading time is switched off in the settings.', 'smartengin-analytics' ) . '</p>';
    126136                }
    127                 $total = 0;
    128                 foreach ( $sum as $per ) {
    129                         foreach ( $per as $x ) {
    130                                 $total += $x['all'];
    131                         }
     137                self::start_note( $r, $sday, $reach );
     138                $total = SEAN_Vitals::count_all( $sum );
     139                $none  = $reach
     140                        ? __( 'No measurements since the starting point yet. They appear as soon as visitors open pages – a page view counts once the visitor stays 10 seconds or leaves the page.', 'smartengin-analytics' )
     141                        : __( 'No measurements in this period yet. They appear as soon as visitors open pages – a page view counts once the visitor stays 10 seconds or leaves the page.', 'smartengin-analytics' );
     142                if ( 0 === $total && ! $before ) {
     143                        echo '<p class="sean-empty">' . esc_html( $none ) . '</p>';
     144                        if ( '' === $sday ) {
     145                                self::start_form( $r );
     146                        }
     147                        echo '</div>';
     148                        return;
    132149                }
    133150                if ( 0 === $total ) {
    134                         echo '<p class="sean-empty">' . esc_html__( 'No measurements in this period yet. They appear as soon as visitors open pages – a page view counts once the visitor stays 10 seconds or leaves the page.', 'smartengin-analytics' ) . '</p></div>';
    135                         return;
     151                        echo '<p class="sean-empty">' . esc_html( $none ) . '</p>';
    136152                }
    137153                $pass = array();
    … …  
    150166                                echo '<td data-dev="' . esc_attr( $devs[ $d ] ) . '">';
    151167                                if ( $x['all'] < 1 ) {
    152                                         echo '<span class="sean-muted">' . esc_html__( 'not measured', 'smartengin-analytics' ) . '</span></td>';
     168                                        echo '<span class="sean-muted">' . esc_html__( 'not measured', 'smartengin-analytics' ) . '</span>';
     169                                        self::before_value( $before, $m, $d );
     170                                        echo '</td>';
    153171                                        continue;
    154172                                }
    … …  
    172190                                /* translators: %s: number of measurements */
    173191                                echo esc_html( sprintf( _n( '%s measurement', '%s measurements', $x['all'], 'smartengin-analytics' ), number_format_i18n( $x['all'] ) ) ) . ' · ' . esc_html( $tip );
    174                                 echo '</span></td>';
     192                                echo '</span>';
     193                                self::before_value( $before, $m, $d );
     194                                echo '</td>';
    175195                        }
    176196                        echo '</tr>';
    … …  
    183203                );
    184204                foreach ( array_keys( $devs ) as $d ) {
    185                         echo '<td data-dev="' . esc_attr( $devs[ $d ] ) . '"><span class="sean-cwv sean-cwv--' . esc_attr( $pass[ $d ] ) . '">' . esc_html( $pw[ $pass[ $d ] ] ) . '</span></td>';
     205                        echo '<td data-dev="' . esc_attr( $devs[ $d ] ) . '"><span class="sean-cwv sean-cwv--' . esc_attr( $pass[ $d ] ) . '">' . esc_html( $pw[ $pass[ $d ] ] ) . '</span>';
     206                        if ( $before ) {
     207                                /* translators: %s: value before the starting point, e.g. "1.0 s" or "Not passed" */
     208                                echo '<span class="sean-muted sean-cwv-before">' . esc_html( sprintf( __( 'before: %s', 'smartengin-analytics' ), $pw[ SEAN_Vitals::passed( $before, $d ) ] ) ) . '</span>';
     209                        }
     210                        echo '</td>';
    186211                }
    187212                echo '</tr></tbody></table></div>';
    … …  
    193218                 */
    194219                do_action( 'sean_tech_after_vitals', $r );
     220                if ( '' === $sday ) {
     221                        self::start_form( $r );
     222                }
    195223                echo '</div>';
     224        }
     225
     226        /**
     227         * Under a value: the 75th percentile of the 28 days before the starting point.
     228         *
     229         * @param array|null $before Summary before the starting point, or null.
     230         * @param string     $m      Metric.
     231         * @param string     $d      Device.
     232         * @return void
     233         */
     234        private static function before_value( $before, $m, $d ) {
     235                if ( ! $before || $before[ $m ][ $d ]['all'] < 1 || null === $before[ $m ][ $d ]['p75'] ) {
     236                        return;
     237                }
     238                /* translators: %s: value before the starting point, e.g. "1.0 s" or "Not passed" */
     239                echo '<span class="sean-muted sean-cwv-before">' . esc_html( sprintf( __( 'before: %s', 'smartengin-analytics' ), SEAN_Vitals::format( $m, $before[ $m ][ $d ]['p75'] ) ) ) . '</span>';
     240        }
     241
     242        /**
     243         * Hidden fields that bring the chosen period back after a form.
     244         *
     245         * @param array $r Range.
     246         * @return void
     247         */
     248        private static function range_fields( array $r ) {
     249                $r = wp_parse_args(
     250                        $r,
     251                        array(
     252                                'key'  => 'custom',
     253                                'from' => wp_date( 'Y-m-d' ),
     254                                'to'   => wp_date( 'Y-m-d' ),
     255                                'back' => 0,
     256                        )
     257                );
     258                foreach ( SEAN_Dashboard::range_args( $r ) as $k => $v ) {
     259                        echo '<input type="hidden" name="sean_r_' . esc_attr( $k ) . '" value="' . esc_attr( (string) $v ) . '">';
     260                }
     261        }
     262
     263        /**
     264         * Note above the values while a starting point is set, with the button to remove it.
     265         *
     266         * @param array  $r     Range.
     267         * @param string $sday  Day of the starting point or ''.
     268         * @param bool   $reach The period reaches the starting point.
     269         * @return void
     270         */
     271        private static function start_note( array $r, $sday, $reach ) {
     272                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only.
     273                $msg = isset( $_GET['sean_msg'] ) ? sanitize_key( wp_unslash( $_GET['sean_msg'] ) ) : '';
     274                if ( 'vitals_start_removed' === $msg ) {
     275                        echo '<p class="sean-vitals-start__done">' . esc_html__( 'Starting point removed – all measurements count again.', 'smartengin-analytics' ) . '</p>';
     276                }
     277                if ( '' === $sday ) {
     278                        return;
     279                }
     280                $when = wp_date( get_option( 'date_format' ) . ', ' . get_option( 'time_format' ), SEAN_Vitals::start_ts() );
     281                echo '<div class="sean-vitals-start sean-vitals-start--set">';
     282                if ( 'vitals_start' === $msg ) {
     283                        echo '<p class="sean-vitals-start__done">' . esc_html__( 'Starting point set.', 'smartengin-analytics' ) . '</p>';
     284                }
     285                if ( $reach ) {
     286                        /* translators: %s: date and time of the starting point */
     287                        echo '<p><strong>' . esc_html( sprintf( __( 'Measured afresh since %s (your starting point after the optimisation).', 'smartengin-analytics' ), $when ) ) . '</strong> ' . esc_html__( 'Measurements before it no longer count here, in the reports and in the alerts. Nothing was deleted: under each value, “before” shows the 28 days before the starting point for comparison.', 'smartengin-analytics' ) . '</p>';
     288                } else {
     289                        /* translators: %s: date and time of the starting point */
     290                        echo '<p>' . esc_html( sprintf( __( 'This period lies before your starting point (%s) – these are the values from back then.', 'smartengin-analytics' ), $when ) ) . '</p>';
     291                }
     292                echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="sean-inline-form">';
     293                wp_nonce_field( 'sean_vitals_start' );
     294                self::range_fields( $r );
     295                echo '<input type="hidden" name="action" value="sean_vitals_start"><input type="hidden" name="sean_remove" value="1"><button class="button button-small">' . esc_html__( 'Remove starting point', 'smartengin-analytics' ) . '</button></form>';
     296                echo '</div>';
     297        }
     298
     299        /**
     300         * Form "Website optimised? Measure afresh from …" (no starting point yet).
     301         *
     302         * @param array $r Range.
     303         * @return void
     304         */
     305        private static function start_form( array $r ) {
     306                $today = wp_date( 'Y-m-d' );
     307                echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="sean-vitals-start" id="sean-vitals-start">';
     308                wp_nonce_field( 'sean_vitals_start' );
     309                self::range_fields( $r );
     310                echo '<input type="hidden" name="action" value="sean_vitals_start">';
     311                echo '<p><strong>' . esc_html__( 'Website optimised?', 'smartengin-analytics' ) . '</strong> ' . esc_html__( 'Set a starting point: from then on only new measurements count – here, in the reports and in the alerts. Nothing is deleted; the values before it are shown next to the new ones for comparison. Choose today (then only page views from this moment count) or the day after your work was finished. Empty the page cache first, so visitors get the optimised pages.', 'smartengin-analytics' ) . '</p>';
     312                echo '<p><label>' . esc_html__( 'Measure afresh from', 'smartengin-analytics' ) . ' <input type="date" name="sean_start_day" value="' . esc_attr( $today ) . '" max="' . esc_attr( $today ) . '"></label> <button class="button">' . esc_html__( 'Set starting point', 'smartengin-analytics' ) . '</button></p>';
     313                echo '</form>';
    196314        }
    197315
  • smartengin-analytics/trunk/includes/class-sean-link-check.php

    r3744444 r3793353  
    559559                                                'edit'  => (string) get_edit_term_link( $term ),
    560560                                                'view'  => is_wp_error( $term_link ) ? '' : self::view_url( (string) $term_link, (string) $r['text'], (string) $src_row['ctx'] ),
     561                                                'ctx'   => (string) $src_row['ctx'],
    561562                                        );
    562563                                        continue;
    … …  
    574575                                                'edit'  => admin_url( 'nav-menus.php' . ( ( ! is_wp_error( $menus ) && $menus ) ? '?action=edit&menu=' . (int) $menus[0]->term_id : '' ) ),
    575576                                                'view'  => self::view_url( home_url( '/' ), (string) $r['text'] ),
     577                                                'ctx'   => '',
    576578                                        );
    577579                                } else {
    … …  
    610612                                                'edit'  => self::edit_url( $p ),
    611613                                                'view'  => self::view_url( self::place_page( $p ), (string) $r['text'], (string) $src_row['ctx'] ),
     614                                                'ctx'   => (string) $src_row['ctx'],
    612615                                        );
    613616                                }
  • smartengin-analytics/trunk/includes/class-sean-vitals.php

    r3744444 r3793353  
    7878
    7979        /**
     80         * Option: starting point "measure afresh from here" (Unix time), set after optimising the website
     81         * (Pierre 09.11.2026). Nothing is deleted: the values before it stay and are shown for comparison.
     82         */
     83        const START_OPTION = 'sean_vitals_start';
     84
     85        /**
     86         * Starting point (Unix time) or 0.
     87         *
     88         * @return int
     89         */
     90        public static function start_ts() {
     91                return max( 0, (int) get_option( self::START_OPTION, 0 ) );
     92        }
     93
     94        /**
     95         * Day of the starting point (site time) or ''.
     96         *
     97         * @return string
     98         */
     99        public static function start_day() {
     100                $t = self::start_ts();
     101                return $t > 0 ? wp_date( 'Y-m-d', $t ) : '';
     102        }
     103
     104        /**
     105         * From which time page views of a day count: on the day of the starting point from the starting
     106         * point on, every other day from 0 (all).
     107         *
     108         * @param string $day Day.
     109         * @return int
     110         */
     111        public static function day_floor( $day ) {
     112                $t = self::start_ts();
     113                return ( $t > 0 && wp_date( 'Y-m-d', $t ) === $day ) ? $t : 0;
     114        }
     115
     116        /**
     117         * First day of a period that counts: never before the starting point (when the period reaches it).
     118         *
     119         * @param string $from Day.
     120         * @param string $to   Day.
     121         * @return string
     122         */
     123        public static function clamp( $from, $to ) {
     124                $s = self::start_day();
     125                if ( '' === $s || $to < $s || $from >= $s ) {
     126                        return $from;
     127                }
     128                return $s;
     129        }
     130
     131        /**
     132         * Set the starting point. A day whose page views are still kept (today, yesterday) is folded again
     133         * at once, so the measurements before the starting point leave that day.
     134         *
     135         * @param int $ts Unix time.
     136         * @return void
     137         */
     138        public static function set_start( $ts ) {
     139                $old = self::start_day();
     140                update_option( self::START_OPTION, max( 1, (int) $ts ), false );
     141                self::refold( self::start_day() );
     142                if ( '' !== $old && $old !== self::start_day() ) {
     143                        self::refold( $old );
     144                }
     145        }
     146
     147        /**
     148         * Remove the starting point (all measurements count again).
     149         *
     150         * @return void
     151         */
     152        public static function clear_start() {
     153                $old = self::start_day();
     154                delete_option( self::START_OPTION );
     155                if ( '' !== $old ) {
     156                        self::refold( $old );
     157                }
     158        }
     159
     160        /**
     161         * Fold a day again, but only while its page views are still kept (today and yesterday) –
     162         * an older day would lose its figures.
     163         *
     164         * @param string $day Day.
     165         * @return void
     166         */
     167        private static function refold( $day ) {
     168                if ( '' === $day || $day < wp_date( 'Y-m-d', time() - DAY_IN_SECONDS ) || $day > wp_date( 'Y-m-d' ) ) {
     169                        return;
     170                }
     171                SEAN_Aggregator::recompute_day( $day );
     172        }
     173
     174        /**
    80175         * Fold the measured page views of one day (called from SEAN_Aggregator::recompute_day()).
    81176         *
    … …  
    88183                $views    = SEAN_DB::views();
    89184                $sessions = SEAN_DB::sessions();
     185                $floor    = self::day_floor( $day ); // On the day of the starting point only page views after it (1.0.12).
    90186                $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE day = %s AND dim IN ('cwv','cwv_h')", $dim, $day ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery
    91187                foreach ( array_keys( self::METRICS ) as $m ) {
    … …  
    97193                        $key  = $m . '|';
    98194                        // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery -- metric names and limits from the fixed list above, all as placeholders.
    99                         $wpdb->query( $wpdb->prepare( "INSERT INTO %i (day, dim, k, visitors, pageviews, dur_sum, dur_n) SELECT %s, 'cwv', CONCAT(%s, " . self::DEVICE_SQL . ", '|', CASE WHEN v.%i <= %d THEN 'good' WHEN v.%i <= %d THEN 'ni' ELSE 'poor' END) AS kk, COUNT(*), COUNT(*), 0, 0" . ' FROM %i v JOIN %i s ON s.id = v.session_id WHERE v.day = %s AND v.%i IS NOT NULL GROUP BY kk', $dim, $day, $key, $m, $good, $m, $poor, $views, $sessions, $day, $m ) );
    100                         $wpdb->query( $wpdb->prepare( "INSERT INTO %i (day, dim, k, visitors, pageviews, dur_sum, dur_n) SELECT %s, 'cwv_h', CONCAT(%s, " . self::DEVICE_SQL . ", '|', LEAST(%d, FLOOR(v.%i / %d))) AS kk, COUNT(*), COUNT(*), 0, 0" . ' FROM %i v JOIN %i s ON s.id = v.session_id WHERE v.day = %s AND v.%i IS NOT NULL GROUP BY kk', $dim, $day, $key, $bins, $m, $step, $views, $sessions, $day, $m ) );
     195                        $wpdb->query( $wpdb->prepare( "INSERT INTO %i (day, dim, k, visitors, pageviews, dur_sum, dur_n) SELECT %s, 'cwv', CONCAT(%s, " . self::DEVICE_SQL . ", '|', CASE WHEN v.%i <= %d THEN 'good' WHEN v.%i <= %d THEN 'ni' ELSE 'poor' END) AS kk, COUNT(*), COUNT(*), 0, 0" . ' FROM %i v JOIN %i s ON s.id = v.session_id WHERE v.day = %s AND v.ts >= %d AND v.%i IS NOT NULL GROUP BY kk', $dim, $day, $key, $m, $good, $m, $poor, $views, $sessions, $day, $floor, $m ) );
     196                        $wpdb->query( $wpdb->prepare( "INSERT INTO %i (day, dim, k, visitors, pageviews, dur_sum, dur_n) SELECT %s, 'cwv_h', CONCAT(%s, " . self::DEVICE_SQL . ", '|', LEAST(%d, FLOOR(v.%i / %d))) AS kk, COUNT(*), COUNT(*), 0, 0" . ' FROM %i v JOIN %i s ON s.id = v.session_id WHERE v.day = %s AND v.ts >= %d AND v.%i IS NOT NULL GROUP BY kk', $dim, $day, $key, $bins, $m, $step, $views, $sessions, $day, $floor, $m ) );
    101197                        // phpcs:enable
    102198                }
    … …  
    141237         * Summary for a period: per metric and device the counts, shares, 75th percentile, verdict.
    142238         *
    143          * @param string $from Day.
    144          * @param string $to   Day.
     239         * @param string $from  Day.
     240         * @param string $to    Day.
     241         * @param bool   $clamp Only from the starting point on (default); false for "before" comparisons.
    145242         * @return array<string,array<string,array>> metric => device (m|d) => data.
    146243         */
    147         public static function summary( $from, $to ) {
     244        public static function summary( $from, $to, $clamp = true ) {
    148245                global $wpdb;
     246                if ( $clamp ) {
     247                        $from = self::clamp( $from, $to );
     248                }
    149249                $dim = SEAN_DB::dim();
    150250                $out = array();
    … …  
    348448                        return '';
    349449                }
    350                 $prev  = self::summary( $pfrom, $pto );
     450                $prev  = self::summary( $pfrom, $pto, false ); // The period before stays as it was – also before an optimisation.
    351451                $color = array(
    352452                        'good' => '#1e8a4f',
  • smartengin-analytics/trunk/readme.txt

    r3791304 r3793353  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.0.11
     7Stable tag: 1.0.12
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    142142== Changelog ==
    143143
     144= 1.0.12 =
     145* Loading time: after optimising the website, set a starting point – from then on only new measurements count, the values before stay for comparison.
     146* Broken links: "AI briefing" – a text for an AI chat that guides you through repairing the links.
     147
    144148= 1.0.11 =
    145149* Security log: requests from Stripe, Bingbot and Googlebot are recognised by the official address lists of their makers (shipped with the plugin, nothing is downloaded) and shown by name instead of "human or bot".
  • smartengin-analytics/trunk/smartengin-analytics.php

    r3791304 r3793353  
    44 * Plugin URI:        https://smartengin.de/smartengin-analytics/
    55 * Description:       Privacy-friendly visitor statistics that show where visitors come from – classic search, AI assistants, social media, other websites or direct – plus pages, clicks on buy buttons and links, countries, devices, live visitors and a security log (login attempts, probes, spam) that only records. No cookies, no stored IP addresses in the statistics.
    6  * Version:           1.0.11
     6 * Version:           1.0.12
    77 * Requires at least: 6.8
    88 * Requires PHP:      7.4
    … …  
    1919}
    2020
    21 define( 'SEAN_VERSION', '1.0.11' );
     21define( 'SEAN_VERSION', '1.0.12' );
    2222define( 'SEAN_FILE', __FILE__ );
    2323define( 'SEAN_DIR', plugin_dir_path( __FILE__ ) );
    … …  
    8787                require_once SEAN_DIR . 'includes/admin/class-sean-security-tab.php';
    8888                require_once SEAN_DIR . 'includes/admin/class-sean-tech-tab.php';
     89                require_once SEAN_DIR . 'includes/admin/class-sean-link-brief.php';
    8990                require_once SEAN_DIR . 'includes/admin/class-sean-settings-page.php';
    9091                require_once SEAN_DIR . 'includes/admin/class-sean-export.php';
    … …  
    9394                SEAN_Admin::init();
    9495                SEAN_Tech_Tab::init();
     96                SEAN_Link_Brief::init();
    9597        }
    9698
  • smartengin-analytics/trunk/uninstall.php

    r3744444 r3793353  
    2424                $wpdb->query( $wpdb->prepare( "DROP TABLE IF EXISTS %i", $sean_table ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery
    2525        }
    26         foreach ( array( 'sean_settings', 'sean_db_version', 'sean_version', 'sean_activated_at', 'sean_buffer_key', 'sean_config_hash', 'sean_endpoint_ok', 'sean_endpoint_check', 'sean_geo', 'sean_last_aggregate', 'sean_links_cursor', 'sean_last_lines', 'sean_last_ms', 'sean_hourly_ms', 'sean_hourly_last', 'sean_link_check_force', 'sean_link_check_last', 'sean_report_last' ) as $sean_o ) {
     26        foreach ( array( 'sean_settings', 'sean_db_version', 'sean_version', 'sean_activated_at', 'sean_buffer_key', 'sean_config_hash', 'sean_endpoint_ok', 'sean_endpoint_check', 'sean_geo', 'sean_last_aggregate', 'sean_links_cursor', 'sean_last_lines', 'sean_last_ms', 'sean_hourly_ms', 'sean_hourly_last', 'sean_link_check_force', 'sean_link_check_last', 'sean_report_last', 'sean_vitals_start' ) as $sean_o ) {
    2727                delete_option( $sean_o );
    2828        }
Note: See TracChangeset for help on using the changeset viewer.