Changeset 3793353
- Timestamp:
- 11/09/2026 10:20:28 PM (41 hours ago)
- Location:
- smartengin-analytics/trunk
- Files:
-
- 1 added
- 10 edited
-
assets/css/admin.css (modified) (1 diff)
-
assets/js/admin-tech.js (modified) (1 diff)
-
help/en.php (modified) (2 diffs)
-
includes/admin/class-sean-link-brief.php (added)
-
includes/admin/class-sean-tech-tab.php (modified) (3 diffs)
-
includes/class-sean-extras.php (modified) (6 diffs)
-
includes/class-sean-link-check.php (modified) (3 diffs)
-
includes/class-sean-vitals.php (modified) (5 diffs)
-
readme.txt (modified) (2 diffs)
-
smartengin-analytics.php (modified) (4 diffs)
-
uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
smartengin-analytics/trunk/assets/css/admin.css
r3791304 r3793353 425 425 /* 1.0.11: filter dropdowns never wider than the screen (long result names on phones). */ 426 426 .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 76 76 } 77 77 })(); 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 89 89 '<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> 90 90 <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. 92 92 <ul> 93 93 <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> … … 104 104 </ul> 105 105 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. 106 107 <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> 107 108 <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 25 25 add_action( 'admin_post_sean_link_check', array( __CLASS__, 'check_now' ) ); 26 26 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; 27 64 } 28 65 … … 190 227 } else { 191 228 self::problem_table( $p ); 229 self::brief_box(); 192 230 } 193 231 // Links that could not be checked (Pierre 10.10.2026: fold-out with a very discreet orange note). … … 201 239 } 202 240 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>'; 203 266 } 204 267 -
smartengin-analytics/trunk/includes/class-sean-extras.php
r3744444 r3793353 103 103 return; 104 104 } 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 } 106 116 $devs = array( 107 117 'm' => __( 'Phone & tablet', 'smartengin-analytics' ), … … 125 135 echo '<p class="sean-hint">' . esc_html__( 'Measuring the loading time is switched off in the settings.', 'smartengin-analytics' ) . '</p>'; 126 136 } 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; 132 149 } 133 150 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>'; 136 152 } 137 153 $pass = array(); … … 150 166 echo '<td data-dev="' . esc_attr( $devs[ $d ] ) . '">'; 151 167 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>'; 153 171 continue; 154 172 } … … 172 190 /* translators: %s: number of measurements */ 173 191 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>'; 175 195 } 176 196 echo '</tr>'; … … 183 203 ); 184 204 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>'; 186 211 } 187 212 echo '</tr></tbody></table></div>'; … … 193 218 */ 194 219 do_action( 'sean_tech_after_vitals', $r ); 220 if ( '' === $sday ) { 221 self::start_form( $r ); 222 } 195 223 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>'; 196 314 } 197 315 -
smartengin-analytics/trunk/includes/class-sean-link-check.php
r3744444 r3793353 559 559 'edit' => (string) get_edit_term_link( $term ), 560 560 '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'], 561 562 ); 562 563 continue; … … 574 575 'edit' => admin_url( 'nav-menus.php' . ( ( ! is_wp_error( $menus ) && $menus ) ? '?action=edit&menu=' . (int) $menus[0]->term_id : '' ) ), 575 576 'view' => self::view_url( home_url( '/' ), (string) $r['text'] ), 577 'ctx' => '', 576 578 ); 577 579 } else { … … 610 612 'edit' => self::edit_url( $p ), 611 613 'view' => self::view_url( self::place_page( $p ), (string) $r['text'], (string) $src_row['ctx'] ), 614 'ctx' => (string) $src_row['ctx'], 612 615 ); 613 616 } -
smartengin-analytics/trunk/includes/class-sean-vitals.php
r3744444 r3793353 78 78 79 79 /** 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 /** 80 175 * Fold the measured page views of one day (called from SEAN_Aggregator::recompute_day()). 81 176 * … … 88 183 $views = SEAN_DB::views(); 89 184 $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). 90 186 $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 91 187 foreach ( array_keys( self::METRICS ) as $m ) { … … 97 193 $key = $m . '|'; 98 194 // 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 ) ); 101 197 // phpcs:enable 102 198 } … … 141 237 * Summary for a period: per metric and device the counts, shares, 75th percentile, verdict. 142 238 * 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. 145 242 * @return array<string,array<string,array>> metric => device (m|d) => data. 146 243 */ 147 public static function summary( $from, $to ) {244 public static function summary( $from, $to, $clamp = true ) { 148 245 global $wpdb; 246 if ( $clamp ) { 247 $from = self::clamp( $from, $to ); 248 } 149 249 $dim = SEAN_DB::dim(); 150 250 $out = array(); … … 348 448 return ''; 349 449 } 350 $prev = self::summary( $pfrom, $pto );450 $prev = self::summary( $pfrom, $pto, false ); // The period before stays as it was – also before an optimisation. 351 451 $color = array( 352 452 'good' => '#1e8a4f', -
smartengin-analytics/trunk/readme.txt
r3791304 r3793353 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.0.1 17 Stable tag: 1.0.12 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 142 142 == Changelog == 143 143 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 144 148 = 1.0.11 = 145 149 * 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 4 4 * Plugin URI: https://smartengin.de/smartengin-analytics/ 5 5 * 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.1 16 * Version: 1.0.12 7 7 * Requires at least: 6.8 8 8 * Requires PHP: 7.4 … … 19 19 } 20 20 21 define( 'SEAN_VERSION', '1.0.1 1' );21 define( 'SEAN_VERSION', '1.0.12' ); 22 22 define( 'SEAN_FILE', __FILE__ ); 23 23 define( 'SEAN_DIR', plugin_dir_path( __FILE__ ) ); … … 87 87 require_once SEAN_DIR . 'includes/admin/class-sean-security-tab.php'; 88 88 require_once SEAN_DIR . 'includes/admin/class-sean-tech-tab.php'; 89 require_once SEAN_DIR . 'includes/admin/class-sean-link-brief.php'; 89 90 require_once SEAN_DIR . 'includes/admin/class-sean-settings-page.php'; 90 91 require_once SEAN_DIR . 'includes/admin/class-sean-export.php'; … … 93 94 SEAN_Admin::init(); 94 95 SEAN_Tech_Tab::init(); 96 SEAN_Link_Brief::init(); 95 97 } 96 98 -
smartengin-analytics/trunk/uninstall.php
r3744444 r3793353 24 24 $wpdb->query( $wpdb->prepare( "DROP TABLE IF EXISTS %i", $sean_table ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery 25 25 } 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 ) { 27 27 delete_option( $sean_o ); 28 28 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)