PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.1
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / living-tree / helpers.php

helpers.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.1, at includes/living-tree/helpers.php

351 lines 11.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Living Tree: metric helpers.
4 *
5 * The scalar signals the snapshot builder folds into the site's DNA.
6 * Each helper composes existing WordPress aggregates (`wp_count_posts`,
7 * `wp_count_comments`, `wp_count_terms`), the site-views traffic signal,
8 * and framework presence — never a per-row payload. The golden rule
9 * (WordPress emits hormones, never geometry) starts here: everything
10 * returned is a scalar or a tiny capped list.
11 *
12 * @package OpenStation
13 */
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * The site's inception moment as a unix timestamp — the stable half of
19 * the determinism seed (`siteUrl|installEpoch`), so it must never drift
20 * between requests. Composed (core has no first-party "install time"):
21 * the earlier of the oldest user registration and the oldest published
22 * post date.
23 *
24 * @return int Unix timestamp, or 0 when the site has neither.
25 */
26 function openstation_living_tree_install_epoch() {
27 global $wpdb;
28
29 $oldest_user = $wpdb->get_var(
30 "SELECT MIN( user_registered ) FROM {$wpdb->users}"
31 );
32 $oldest_post = $wpdb->get_var(
33 "SELECT MIN( post_date_gmt ) FROM {$wpdb->posts}
34 WHERE post_status = 'publish' AND post_date_gmt > '1970-01-01 00:00:01'"
35 );
36
37 $candidates = array();
38 foreach ( array( $oldest_user, $oldest_post ) as $mysql_date ) {
39 if ( $mysql_date ) {
40 $ts = strtotime( $mysql_date . ' UTC' );
41 if ( $ts > 0 ) {
42 $candidates[] = $ts;
43 }
44 }
45 }
46
47 return empty( $candidates ) ? 0 : min( $candidates );
48 }
49
50 /**
51 * Age of the site in whole days, from the install epoch. Clamped to be
52 * non-negative — the master clock never runs backwards.
53 *
54 * @return int Whole days since the site's inception. >= 0.
55 */
56 function openstation_living_tree_site_age_days() {
57 $epoch = openstation_living_tree_install_epoch();
58 if ( $epoch <= 0 ) {
59 return 0;
60 }
61 return max( 0, (int) floor( ( time() - $epoch ) / DAY_IN_SECONDS ) );
62 }
63
64 /**
65 * Recent traffic signal, resolved with the same source ladder the
66 * site-views widget uses: Jetpack Stats first, then the
67 * `_post_views_YYYY-MM-DD` post-meta convention — both summed over the
68 * last 14 days. Sites with neither simply report 0 (a windless day).
69 *
70 * The final value passes through the `openstation_living_tree_traffic`
71 * filter so analytics plugins with their own counters can feed the
72 * real number in.
73 *
74 * @return int Recent view sum. >= 0.
75 */
76 function openstation_living_tree_traffic() {
77 $views = openstation_living_tree_jetpack_visits();
78 if ( null === $views ) {
79 $views = openstation_living_tree_meta_views();
80 }
81
82 /**
83 * Filter the Living Tree traffic hormone source. Return a
84 * non-negative view count for the last ~14 days — it drives the
85 * wind (canopy sway amplitude / frequency).
86 *
87 * @param int $views Views in the window. Default: Jetpack Stats
88 * when available, else the `_post_views_*` meta
89 * sum, else 0.
90 */
91 $views = (int) apply_filters( 'openstation_living_tree_traffic', $views );
92 return max( 0, $views );
93 }
94
95 /**
96 * Last-14-days visits from Jetpack Stats, or `null` when unavailable.
97 *
98 * Reads through `Automattic\Jetpack\Stats\WPCOM_Stats::get_visits()` —
99 * the same WPCOM endpoint the `jetpack/v4/stats/visits` REST route
100 * (used by the site-views widget's client) proxies, but callable
101 * server-side without a per-user capability check, so the snapshot's
102 * transient cache holds the same value no matter which user primes it.
103 * Any failure — Jetpack absent, no `get_visits` method, WP_Error,
104 * unexpected payload — returns `null` and the caller falls back to the
105 * post-views meta. A successful `0` is trusted (a quiet site is a
106 * valid answer), matching the widget's source-ladder semantics.
107 *
108 * @return int|null Views over the last 14 days, or null when Jetpack
109 * Stats can't answer.
110 */
111 function openstation_living_tree_jetpack_visits() {
112 if ( ! class_exists( '\Automattic\Jetpack\Stats\WPCOM_Stats' ) ) {
113 return null;
114 }
115 $wpcom_stats = new \Automattic\Jetpack\Stats\WPCOM_Stats();
116 if ( ! method_exists( $wpcom_stats, 'get_visits' ) ) {
117 return null;
118 }
119
120 try {
121 $stats = $wpcom_stats->get_visits(
122 array(
123 'unit' => 'day',
124 'quantity' => 14,
125 )
126 );
127 } catch ( \Throwable $e ) {
128 return null;
129 }
130 if ( is_wp_error( $stats ) ) {
131 return null;
132 }
133
134 // Jetpack versions differ on object-vs-assoc-array decoding —
135 // normalise to arrays before reading.
136 $stats = json_decode( wp_json_encode( $stats ), true );
137 if ( ! is_array( $stats ) || empty( $stats['data'] ) || ! is_array( $stats['data'] ) ) {
138 return null;
139 }
140
141 // Rows are positional per the response's `fields` list — usually
142 // array( 'period', 'views' ). Locate 'views' rather than assuming.
143 $views_index = 1;
144 if ( isset( $stats['fields'] ) && is_array( $stats['fields'] ) ) {
145 $idx = array_search( 'views', $stats['fields'], true );
146 if ( false !== $idx ) {
147 $views_index = (int) $idx;
148 }
149 }
150
151 $total = 0;
152 foreach ( $stats['data'] as $row ) {
153 if ( is_array( $row ) && isset( $row[ $views_index ] ) && is_numeric( $row[ $views_index ] ) ) {
154 $total += (int) $row[ $views_index ];
155 }
156 }
157 return $total;
158 }
159
160 /**
161 * Last-14-days view sum from the `_post_views_YYYY-MM-DD` post-meta
162 * convention — the plain-WP fallback shared with the site-views
163 * widget. Sites without a view-counter plugin report 0.
164 *
165 * @return int Recent view sum. >= 0.
166 */
167 function openstation_living_tree_meta_views() {
168 global $wpdb;
169
170 $total = 0;
171 $today = current_time( 'Y-m-d' );
172 for ( $i = 0; $i < 14; $i++ ) {
173 $date = gmdate( 'Y-m-d', strtotime( $today . ' -' . $i . ' days' ) );
174 $meta_key = '_post_views_' . $date;
175 $total += (int) $wpdb->get_var(
176 $wpdb->prepare(
177 "SELECT COALESCE( SUM( CAST( meta_value AS UNSIGNED ) ), 0 )
178 FROM {$wpdb->postmeta}
179 WHERE meta_key = %s",
180 $meta_key
181 )
182 );
183 }
184
185 return max( 0, $total );
186 }
187
188 /**
189 * Number of users currently online, from framework presence.
190 *
191 * @return int Count of users with `online` presence status. >= 0.
192 */
193 function openstation_living_tree_active_users() {
194 if ( ! function_exists( 'openstation_presence_snapshot' ) ) {
195 return 0;
196 }
197 $count = 0;
198 foreach ( openstation_presence_snapshot() as $record ) {
199 if ( isset( $record['status'] ) && 'online' === $record['status'] ) {
200 ++$count;
201 }
202 }
203 return $count;
204 }
205
206 /**
207 * SEO / site-health score, normalised 0..1.
208 *
209 * KNOWN GAP: unlike `traffic` (Jetpack Stats → post-views meta) and
210 * `performance` (core Site Health tallies), this hormone still has no
211 * first-party source — WordPress ships nothing SEO-shaped to read, so
212 * the default is a healthy 0.7 and the filter is the only integration
213 * point. Candidate future source: aggregate the per-post scores that
214 * SEO plugins store in post-meta into a site-wide average. Until then,
215 * an SEO or monitoring plugin that *does* know the site's health can
216 * feed the real value in via the filter.
217 *
218 * @return float Health score in [0, 1].
219 */
220 function openstation_living_tree_seo_health() {
221 /**
222 * Filter the Living Tree health hormone source. Return 0..1 — it
223 * drives the canopy's colour temperature (green → yellow → red →
224 * grey).
225 *
226 * @param float $health Default 0.7.
227 */
228 $health = (float) apply_filters( 'openstation_living_tree_seo_health', 0.7 );
229 return min( 1.0, max( 0.0, $health ) );
230 }
231
232 /**
233 * Performance headroom, normalised 0..1 (1 = plenty, 0 = under load).
234 *
235 * Sourced from core's own Site Health tallies when available (see
236 * {@see openstation_living_tree_site_health_performance()}), falling
237 * back to a comfortable 0.8 until the weekly Site Health cron has run
238 * at least once. The filter remains the integration point for plugins
239 * with real runtime telemetry.
240 *
241 * @return float Performance score in [0, 1].
242 */
243 function openstation_living_tree_performance() {
244 $performance = openstation_living_tree_site_health_performance();
245 if ( null === $performance ) {
246 $performance = 0.8;
247 }
248
249 /**
250 * Filter the Living Tree performance hormone source. Return 0..1 —
251 * it throttles growth vigour.
252 *
253 * @param float $performance Default: a composite of core's Site
254 * Health issue counts when the
255 * `health-check-site-status-result`
256 * transient exists, else 0.8.
257 */
258 $performance = (float) apply_filters( 'openstation_living_tree_performance', $performance );
259 return min( 1.0, max( 0.0, $performance ) );
260 }
261
262 /**
263 * Performance composite from core's Site Health tallies, or `null`
264 * when unavailable.
265 *
266 * WordPress runs every Site Health test on a weekly cron
267 * (`wp_site_health_scheduled_check`) and persists the tallies in the
268 * `health-check-site-status-result` transient as JSON counts
269 * (`good` / `recommended` / `critical`) — the same source the
270 * dashboard's Site Health widget reads. Mapping: start at 1.0,
271 * subtract 0.15 per critical issue and 0.04 per recommendation, clamp
272 * to [0.2, 1] — a clean install grows vigorously, a neglected one
273 * visibly slows down but never fully stalls.
274 *
275 * Site Health measures broad install health (PHP version, HTTPS,
276 * updates, object caching…), not pure runtime speed — the right
277 * flavour for a "growth vigour" hormone. The transient is absent on a
278 * brand-new site until the weekly cron first fires or someone opens
279 * the Site Health screen; callers fall back to the 0.8 default then.
280 *
281 * @return float|null Composite in [0.2, 1], or null when the Site
282 * Health tallies aren't available (yet).
283 */
284 function openstation_living_tree_site_health_performance() {
285 $raw = get_transient( 'health-check-site-status-result' );
286 if ( is_string( $raw ) && '' !== $raw ) {
287 $counts = json_decode( $raw, true );
288 } elseif ( is_array( $raw ) ) {
289 // Defensive: some object-cache drop-ins hand back the decoded
290 // array. Core itself always stores a JSON string.
291 $counts = $raw;
292 } else {
293 return null;
294 }
295
296 if ( ! is_array( $counts )
297 || ( ! isset( $counts['critical'] ) && ! isset( $counts['recommended'] ) && ! isset( $counts['good'] ) )
298 ) {
299 return null;
300 }
301
302 $critical = max( 0, (int) ( $counts['critical'] ?? 0 ) );
303 $recommended = max( 0, (int) ( $counts['recommended'] ?? 0 ) );
304
305 $score = 1.0 - ( 0.15 * $critical ) - ( 0.04 * $recommended );
306 return min( 1.0, max( 0.2, $score ) );
307 }
308
309 /**
310 * Compact per-region structural hints (the `branches` array): published
311 * posts grouped by year, each year mapped to a depth/girth/length hint
312 * normalised against the busiest year. This is DNA, not geometry — the
313 * simulator may bias growth density with it, never position anything.
314 *
315 * @return array[] Compact branch DNA hints (max 12 entries).
316 */
317 function openstation_living_tree_branch_dna() {
318 global $wpdb;
319
320 $rows = $wpdb->get_results(
321 "SELECT YEAR( post_date_gmt ) AS y, COUNT(*) AS n
322 FROM {$wpdb->posts}
323 WHERE post_status = 'publish' AND post_type = 'post'
324 AND post_date_gmt > '1970-01-01 00:00:01'
325 GROUP BY y
326 ORDER BY y ASC
327 LIMIT 12",
328 ARRAY_A
329 );
330 if ( empty( $rows ) ) {
331 return array();
332 }
333
334 $max = 1;
335 foreach ( $rows as $row ) {
336 $max = max( $max, (int) $row['n'] );
337 }
338
339 $out = array();
340 $depth = 0;
341 foreach ( $rows as $row ) {
342 $out[] = array(
343 'depth' => $depth,
344 'girth' => round( (int) $row['n'] / $max, 3 ),
345 'length' => round( min( 1.0, (int) $row['n'] / $max + 0.2 ), 3 ),
346 );
347 ++$depth;
348 }
349 return $out;
350 }
351