chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
<?php
|
||||
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: native postmeta count for dashboard widget
|
||||
/**
|
||||
* TMDO_Dashboard_Widget — wp-admin home dashboard widget (v2.4.0 M9).
|
||||
*
|
||||
* Adds a "WP Data Optimizer 健康狀態" widget on the WP admin dashboard for
|
||||
* users with `manage_options`. Surfaces:
|
||||
* - Today's health check status (green / yellow / red traffic light)
|
||||
* - Consecutive green days streak
|
||||
* - 3 quick stats:last snapshot age, largest zone table, oldest open conflict
|
||||
* - Quick links to Run Health Check, Entity Bridge, Create Snapshot
|
||||
*
|
||||
* Uses transient cache (5 minutes) to keep the dashboard responsive.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard widget — stateless static API.
|
||||
*/
|
||||
class TMDO_Dashboard_Widget {
|
||||
|
||||
public const WIDGET_ID = 'wpdo_health_widget';
|
||||
public const CACHE_TTL = 300;
|
||||
|
||||
/**
|
||||
* Register the dashboard widget hook.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register(): void {
|
||||
add_action( 'wp_dashboard_setup', array( __CLASS__, 'add_widget' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the widget if user has manage_options.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_widget(): void {
|
||||
if ( ! TMDO_Capability::current_user_can_admin() ) {
|
||||
return;
|
||||
}
|
||||
wp_add_dashboard_widget(
|
||||
self::WIDGET_ID,
|
||||
__( 'WP Data Optimizer 健康狀態', '2meet-data-optimizer' ),
|
||||
array( __CLASS__, 'render' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the widget HTML.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render(): void {
|
||||
$page_url = admin_url( 'tools.php?page=wp-data-optimizer' );
|
||||
$run_health_url = wp_nonce_url( add_query_arg( array( 'wpdo_run_health' => '1' ), $page_url ), 'wpdo_run_health' );
|
||||
$create_snap_url = wp_nonce_url( add_query_arg( array( 'wpdo_create_snapshot' => '1' ), $page_url ), 'wpdo_create_snapshot' );
|
||||
$bridge_url = add_query_arg( 'tab', 'entity-bridge', $page_url );
|
||||
|
||||
$status = self::compute_status();
|
||||
$light = self::traffic_light( $status['level'] );
|
||||
?>
|
||||
<style>
|
||||
.wpdo-widget-light { display: inline-block; width: 16px; height: 16px; border-radius: 50%; vertical-align: middle; margin-right: 6px; }
|
||||
.wpdo-widget-light.good { background: #46b450; box-shadow: 0 0 8px rgba(70,180,80,0.5); }
|
||||
.wpdo-widget-light.warn { background: #dba617; box-shadow: 0 0 8px rgba(219,166,23,0.5); }
|
||||
.wpdo-widget-light.crit { background: #dc3232; box-shadow: 0 0 8px rgba(220,50,50,0.5); }
|
||||
.wpdo-widget-light.gray { background: #c3c4c7; }
|
||||
.wpdo-widget-stat { display: flex; justify-content: space-between; padding: 0.4em 0; border-bottom: 1px solid #f0f0f1; }
|
||||
.wpdo-widget-stat:last-child { border-bottom: none; }
|
||||
.wpdo-widget-actions { margin-top: 0.8em; padding-top: 0.8em; border-top: 1px solid #c3c4c7; }
|
||||
.wpdo-widget-actions a { margin-right: 0.5em; }
|
||||
</style>
|
||||
|
||||
<p style="font-size: 1.1em; margin-bottom: 0.6em;">
|
||||
<span class="wpdo-widget-light <?php echo esc_attr( $light['class'] ); ?>"></span>
|
||||
<strong><?php echo esc_html( $light['label'] ); ?></strong>
|
||||
<?php if ( $status['streak'] > 0 ) : ?>
|
||||
·
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: days */
|
||||
esc_html__( '連續綠 %d 天', '2meet-data-optimizer' ),
|
||||
(int) $status['streak']
|
||||
);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
|
||||
<?php if ( '' !== $status['summary_msg'] ) : ?>
|
||||
<p class="description" style="margin-bottom: 0.8em;"><?php echo esc_html( $status['summary_msg'] ); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wpdo-widget-stats">
|
||||
<div class="wpdo-widget-stat">
|
||||
<span><?php esc_html_e( '最近快照', '2meet-data-optimizer' ); ?></span>
|
||||
<span><strong><?php echo esc_html( $status['last_snapshot'] ); ?></strong></span>
|
||||
</div>
|
||||
<div class="wpdo-widget-stat">
|
||||
<span><?php esc_html_e( '最大 zone 表', '2meet-data-optimizer' ); ?></span>
|
||||
<span><strong><?php echo esc_html( $status['largest_zone'] ); ?></strong></span>
|
||||
</div>
|
||||
<div class="wpdo-widget-stat">
|
||||
<span><?php esc_html_e( '未處理衝突', '2meet-data-optimizer' ); ?></span>
|
||||
<span>
|
||||
<strong><?php echo esc_html( (string) (int) $status['conflicts'] ); ?></strong>
|
||||
<?php if ( (int) $status['conflicts'] > 0 ) : ?>
|
||||
<a href="<?php echo esc_url( add_query_arg( 'tab', 'conflicts', $page_url ) ); ?>"
|
||||
style="margin-left:0.5em;"><?php esc_html_e( '查看', '2meet-data-optimizer' ); ?></a>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="wpdo-widget-stat">
|
||||
<span><?php esc_html_e( 'wp_postmeta 行數', '2meet-data-optimizer' ); ?></span>
|
||||
<span><strong><?php echo esc_html( $status['postmeta_human'] ); ?></strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// v2.5.0 M16: actionable module suggestions count.
|
||||
$actionable_count = 0;
|
||||
if ( class_exists( 'TMDO_Module_Detector' ) ) {
|
||||
$cached = TMDO_Module_Detector::get_cached();
|
||||
if ( is_array( $cached ) && isset( $cached['results'] ) ) {
|
||||
foreach ( (array) $cached['results'] as $r ) {
|
||||
// v2.5.0 polish: align threshold with admin tab's get_actionable() default (0.5).
|
||||
if ( ! empty( $r['available'] ) && 'enable' === ( $r['recommendation'] ?? '' ) && (float) ( $r['confidence'] ?? 0 ) >= 0.5 ) {
|
||||
++$actionable_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $actionable_count > 0 ) :
|
||||
$ms_url = add_query_arg( 'tab', 'module-suggestions', $page_url );
|
||||
?>
|
||||
<p style="margin: 0.6em 0; padding: 0.5em 0.7em; background: #fff8e5; border-left: 3px solid #dba617; border-radius: 3px;">
|
||||
🤖
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: count */
|
||||
esc_html__( '偵測到 %d 個建議啟用的 module —', '2meet-data-optimizer' ),
|
||||
(int) $actionable_count
|
||||
);
|
||||
?>
|
||||
<a href="<?php echo esc_url( $ms_url ); ?>"><?php esc_html_e( '看建議', '2meet-data-optimizer' ); ?></a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// v2.8.1: surface user-meta migration residue → CTA to one-click wizard.
|
||||
$attn = class_exists( 'TMDO_Migration_Orchestrator' )
|
||||
? TMDO_Migration_Orchestrator::needs_attention()
|
||||
: array( 'needs' => false );
|
||||
|
||||
if ( ! empty( $attn['needs'] ) && 'running' !== ( $attn['job_state'] ?? '' ) ) :
|
||||
$wizard_url = add_query_arg( 'tab', 'migration-wizard', $page_url );
|
||||
?>
|
||||
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: #fef0f0; border-left: 3px solid #dc3232; border-radius: 3px;">
|
||||
🔴
|
||||
<?php
|
||||
/* translators: 1: EAV row count, 2: group count, 3: ratio. */
|
||||
$msg = __( '偵測到 <strong>%1$s</strong> 行 wp_usermeta EAV 殘留橫跨 <strong>%2$s</strong> 個 entity group(當前 ratio 1:<strong>%3$s</strong>)—', '2meet-data-optimizer' );
|
||||
printf(
|
||||
wp_kses( $msg, array( 'strong' => array() ) ),
|
||||
esc_html( number_format_i18n( (int) $attn['eav_rows'] ) ),
|
||||
esc_html( (string) (int) $attn['groups_with_residue'] ),
|
||||
esc_html( (string) $attn['ratio'] )
|
||||
);
|
||||
?>
|
||||
<a href="<?php echo esc_url( $wizard_url ); ?>"><strong><?php esc_html_e( 'User 遷移精靈 →', '2meet-data-optimizer' ); ?></strong></a>
|
||||
</p>
|
||||
<?php elseif ( 'running' === ( $attn['job_state'] ?? '' ) ) : ?>
|
||||
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: #e5f5fa; border-left: 3px solid #00a0d2; border-radius: 3px;">
|
||||
⏳ <?php esc_html_e( 'User 遷移精靈正在執行中 —', '2meet-data-optimizer' ); ?>
|
||||
<a href="<?php echo esc_url( add_query_arg( 'tab', 'migration-wizard', $page_url ) ); ?>"><?php esc_html_e( '查看進度', '2meet-data-optimizer' ); ?></a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// v2.9.0 Phase 0: wp_postmeta garbage CTA (transients/_wp_old_date/stale _edit_lock).
|
||||
$gc = class_exists( 'TMDO_Postmeta_Cleaner' )
|
||||
? TMDO_Postmeta_Cleaner::count_garbage( TMDO_Postmeta_Cleaner::TARGET_ALL )
|
||||
: array( 'total' => 0 );
|
||||
if ( ! empty( $gc['total'] ) && (int) $gc['total'] > 0 ) :
|
||||
$cleanup_url = wp_nonce_url(
|
||||
add_query_arg( array( 'wpdo_postmeta_cleanup' => '1' ), $page_url ),
|
||||
'wpdo_postmeta_cleanup'
|
||||
);
|
||||
$confirm_msg = sprintf(
|
||||
/* translators: %s: total garbage row count */
|
||||
esc_html__( '即將從 wp_postmeta 刪除 %s 行垃圾資料(transients + _wp_old_date + 過期 _edit_lock)。確認執行?', '2meet-data-optimizer' ),
|
||||
number_format_i18n( (int) $gc['total'] )
|
||||
);
|
||||
?>
|
||||
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: #f6f7f7; border-left: 3px solid #2271b1; border-radius: 3px;">
|
||||
🧹
|
||||
<?php
|
||||
/* translators: 1: total rows, 2: transients, 3: wp_old_date, 4: edit_locks */
|
||||
$gc_msg = __( 'wp_postmeta 偵測到 <strong>%1$s</strong> 行可清理垃圾(transients %2$s + _wp_old_date %3$s + 過期 _edit_lock %4$s)—', '2meet-data-optimizer' );
|
||||
printf(
|
||||
wp_kses( $gc_msg, array( 'strong' => array() ) ),
|
||||
esc_html( number_format_i18n( (int) $gc['total'] ) ),
|
||||
esc_html( number_format_i18n( (int) $gc['transients'] ) ),
|
||||
esc_html( number_format_i18n( (int) $gc['wp_old_date'] ) ),
|
||||
esc_html( number_format_i18n( (int) $gc['edit_locks'] ) )
|
||||
);
|
||||
?>
|
||||
<a href="<?php echo esc_url( $cleanup_url ); ?>"
|
||||
onclick="return confirm(<?php echo wp_json_encode( $confirm_msg ); ?>);">
|
||||
<strong><?php esc_html_e( '一鍵清理 →', '2meet-data-optimizer' ); ?></strong>
|
||||
</a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// v2.9.4: Post Entity diagnostics (independent of user-side needs_attention()).
|
||||
// Shows wp_posts:wp_postmeta ratio, post mode, and total EAV residue
|
||||
// across all 7 entity groups. Read-only summary; full wizard ships v2.9.5.
|
||||
$post_diag = class_exists( 'TMDO_Post_Migration' ) ? TMDO_Post_Migration::diagnose() : null;
|
||||
if ( null !== $post_diag && $post_diag['posts'] > 0 ) :
|
||||
$total_eav = 0;
|
||||
foreach ( $post_diag['groups'] as $g ) {
|
||||
$total_eav += (int) $g['eav_rows'];
|
||||
}
|
||||
$post_color = $total_eav > 0 ? '#dba617' : '#46b450';
|
||||
$post_bg = $total_eav > 0 ? '#fffbe5' : '#ecf7ed';
|
||||
$post_label = $total_eav > 0
|
||||
? sprintf(
|
||||
/* translators: 1: total EAV rows, 2: ratio */
|
||||
__( 'Post Entity:偵測到 <strong>%1$s</strong> 行 wp_postmeta EAV 殘留(當前 ratio 1:<strong>%2$s</strong>,mode=<strong>%3$s</strong>)—', '2meet-data-optimizer' ),
|
||||
'%1$s',
|
||||
'%2$s',
|
||||
'%3$s'
|
||||
)
|
||||
: sprintf(
|
||||
/* translators: 1: ratio */
|
||||
__( 'Post Entity:無 EAV 殘留(ratio 1:<strong>%1$s</strong>,mode=<strong>%2$s</strong>)', '2meet-data-optimizer' ),
|
||||
'%1$s',
|
||||
'%2$s'
|
||||
);
|
||||
?>
|
||||
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: <?php echo esc_attr( $post_bg ); ?>; border-left: 3px solid <?php echo esc_attr( $post_color ); ?>; border-radius: 3px;">
|
||||
📦
|
||||
<?php
|
||||
if ( $total_eav > 0 ) {
|
||||
printf(
|
||||
wp_kses(
|
||||
/* translators: 1: total EAV rows, 2: ratio, 3: mode */
|
||||
__( 'Post Entity:偵測到 <strong>%1$s</strong> 行 wp_postmeta EAV 殘留(當前 ratio 1:<strong>%2$s</strong>,mode=<strong>%3$s</strong>)— v2.9.5 將提供一鍵遷移 UI。', '2meet-data-optimizer' ),
|
||||
array( 'strong' => array() )
|
||||
),
|
||||
esc_html( number_format_i18n( $total_eav ) ),
|
||||
esc_html( (string) $post_diag['ratio'] ),
|
||||
esc_html( $post_diag['mode'] )
|
||||
);
|
||||
} else {
|
||||
printf(
|
||||
wp_kses(
|
||||
/* translators: 1: ratio, 2: mode */
|
||||
__( 'Post Entity:✓ 無 EAV 殘留(ratio 1:<strong>%1$s</strong>,mode=<strong>%2$s</strong>)', '2meet-data-optimizer' ),
|
||||
array( 'strong' => array() )
|
||||
),
|
||||
esc_html( (string) $post_diag['ratio'] ),
|
||||
esc_html( $post_diag['mode'] )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wpdo-widget-actions">
|
||||
<a class="button button-small button-primary" href="<?php echo esc_url( $run_health_url ); ?>">
|
||||
<?php esc_html_e( '跑健康檢查', '2meet-data-optimizer' ); ?>
|
||||
</a>
|
||||
<a class="button button-small" href="<?php echo esc_url( $bridge_url ); ?>">
|
||||
<?php esc_html_e( 'Entity Bridge', '2meet-data-optimizer' ); ?>
|
||||
</a>
|
||||
<a class="button button-small" href="<?php echo esc_url( $create_snap_url ); ?>">
|
||||
<?php esc_html_e( '建立快照', '2meet-data-optimizer' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( $page_url ); ?>" style="float: right; padding-top: 4px;">
|
||||
<?php esc_html_e( '完整儀表板 →', '2meet-data-optimizer' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ─── private ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compute widget status. Cached via transient.
|
||||
*
|
||||
* @return array {level:string, summary_msg:string, streak:int,
|
||||
* last_snapshot:string, largest_zone:string,
|
||||
* conflicts:int, postmeta_human:string}
|
||||
*/
|
||||
private static function compute_status(): array {
|
||||
$cached = get_transient( 'wpdo_dashboard_widget_status' );
|
||||
if ( is_array( $cached ) ) {
|
||||
return $cached;
|
||||
}
|
||||
global $wpdb;
|
||||
|
||||
// Health level from last cron run, falling back to "no data".
|
||||
$last = class_exists( 'TMDO_Health_Cron' ) ? TMDO_Health_Cron::get_last_run() : null;
|
||||
$level = 'unknown';
|
||||
$msg = '';
|
||||
$streak = 0;
|
||||
if ( is_array( $last ) ) {
|
||||
$crit = (int) ( $last['critical_count'] ?? 0 );
|
||||
$rec = (int) ( $last['recommended_count'] ?? 0 );
|
||||
if ( $crit > 0 ) {
|
||||
$level = 'critical';
|
||||
$msg = sprintf( /* translators: %d: count */ __( '%d 項 critical 警告', '2meet-data-optimizer' ), $crit );
|
||||
} elseif ( $rec > 0 ) {
|
||||
$level = 'warn';
|
||||
$msg = sprintf( /* translators: %d: count */ __( '%d 項 recommended 提示', '2meet-data-optimizer' ), $rec );
|
||||
} else {
|
||||
$level = 'good';
|
||||
$streak = TMDO_Health_Cron::consecutive_green_days();
|
||||
}
|
||||
$msg .= ' · ' . sprintf(
|
||||
/* translators: %s: timestamp */
|
||||
__( '最後執行:%s', '2meet-data-optimizer' ),
|
||||
(string) $last['ran_at']
|
||||
);
|
||||
} else {
|
||||
$msg = __( '尚未執行過健康檢查', '2meet-data-optimizer' );
|
||||
}
|
||||
|
||||
// Last snapshot age.
|
||||
$snap_table = $wpdb->prefix . 'wpdo_snapshots';
|
||||
$snap_exists = (int) $wpdb->get_var(
|
||||
$wpdb->prepare( // phpcs:ignore WordPress.DB
|
||||
'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s',
|
||||
$snap_table
|
||||
)
|
||||
);
|
||||
$last_snapshot = __( '從未', '2meet-data-optimizer' );
|
||||
if ( 1 === $snap_exists ) {
|
||||
$ts = (string) $wpdb->get_var( "SELECT created_at FROM `{$snap_table}` ORDER BY created_at DESC LIMIT 1" ); // phpcs:ignore WordPress.DB
|
||||
if ( '' !== $ts ) {
|
||||
$diff = time() - strtotime( $ts . ' UTC' );
|
||||
$last_snapshot = $diff < 0 ? $ts : self::human_time_diff( $diff );
|
||||
}
|
||||
}
|
||||
|
||||
// Largest zone table by row count (rough sample).
|
||||
$largest = '—';
|
||||
if ( 1 === $snap_exists ) {
|
||||
$row_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_warm`" ); // phpcs:ignore WordPress.DB
|
||||
if ( $row_count > 0 ) {
|
||||
$largest = sprintf( 'wpdo_warm (%s)', number_format_i18n( $row_count ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Conflict count (cheap — uses cached summary).
|
||||
$conflicts = 0;
|
||||
if ( class_exists( 'TMDO_Conflict_Monitor' ) ) {
|
||||
$summary = TMDO_Conflict_Monitor::get_summary();
|
||||
$conflicts = (int) ( $summary['total'] ?? 0 );
|
||||
}
|
||||
|
||||
// wp_postmeta size (informational).
|
||||
$pm_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->postmeta}`" ); // phpcs:ignore WordPress.DB
|
||||
|
||||
$result = array(
|
||||
'level' => $level,
|
||||
'summary_msg' => $msg,
|
||||
'streak' => $streak,
|
||||
'last_snapshot' => $last_snapshot,
|
||||
'largest_zone' => $largest,
|
||||
'conflicts' => $conflicts,
|
||||
'postmeta_human' => number_format_i18n( $pm_count ),
|
||||
);
|
||||
set_transient( 'wpdo_dashboard_widget_status', $result, self::CACHE_TTL );
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map level → CSS class + label.
|
||||
*
|
||||
* @param string $level Level slug.
|
||||
* @return array {class:string,label:string}
|
||||
*/
|
||||
private static function traffic_light( string $level ): array {
|
||||
switch ( $level ) {
|
||||
case 'good':
|
||||
return array(
|
||||
'class' => 'good',
|
||||
'label' => __( '正常', '2meet-data-optimizer' ),
|
||||
);
|
||||
case 'warn':
|
||||
return array(
|
||||
'class' => 'warn',
|
||||
'label' => __( '注意', '2meet-data-optimizer' ),
|
||||
);
|
||||
case 'critical':
|
||||
return array(
|
||||
'class' => 'crit',
|
||||
'label' => __( '警告', '2meet-data-optimizer' ),
|
||||
);
|
||||
default:
|
||||
return array(
|
||||
'class' => 'gray',
|
||||
'label' => __( '未知', '2meet-data-optimizer' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable time-diff (seconds → "5 minutes ago" style). Uses WP
|
||||
* `human_time_diff` when available; falls back to simple math otherwise.
|
||||
*
|
||||
* @param int $seconds Seconds delta (>=0).
|
||||
* @return string
|
||||
*/
|
||||
private static function human_time_diff( int $seconds ): string {
|
||||
if ( $seconds < 60 ) {
|
||||
return $seconds . 's';
|
||||
}
|
||||
if ( function_exists( 'human_time_diff' ) ) {
|
||||
return sprintf(
|
||||
/* translators: %s: human-readable time difference */
|
||||
__( '%s 前', '2meet-data-optimizer' ),
|
||||
human_time_diff( time() - $seconds, time() )
|
||||
);
|
||||
}
|
||||
if ( $seconds < 3600 ) {
|
||||
return floor( $seconds / 60 ) . 'm';
|
||||
}
|
||||
if ( $seconds < 86400 ) {
|
||||
return floor( $seconds / 3600 ) . 'h';
|
||||
}
|
||||
return floor( $seconds / 86400 ) . 'd';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user