mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Update dashboard widgets
This commit is contained in:
@@ -16,14 +16,14 @@ class AdminDashboardAutoRefreshTest extends TestCase
|
||||
|
||||
$this->assertStringContainsString('x-data="adminDashboard"', $content);
|
||||
$this->assertStringContainsString('data-data-url="{{ route(\'admin.api.dashboard-data\') }}"', $content);
|
||||
$this->assertStringContainsString('data-refresh-interval="{{ 15 * 60 * 1000 }}"', $content);
|
||||
$this->assertStringContainsString('data-refresh-interval="{{ 60 * 1000 }}"', $content);
|
||||
$this->assertStringContainsString('data-dashboard-content', $content);
|
||||
// The previous full-page-reload approach has been removed in favour of
|
||||
// updating headline tiles + widgets directly from the JSON payload.
|
||||
$this->assertStringNotContainsString('data-refresh-url=', $content);
|
||||
}
|
||||
|
||||
public function test_dashboard_blade_labels_match_fifteen_minute_refresh(): void
|
||||
public function test_dashboard_blade_labels_match_one_minute_refresh(): void
|
||||
{
|
||||
$bladePath = resource_path('views/admin/dashboard.blade.php');
|
||||
|
||||
@@ -31,7 +31,7 @@ class AdminDashboardAutoRefreshTest extends TestCase
|
||||
|
||||
$content = file_get_contents($bladePath);
|
||||
|
||||
$this->assertStringContainsString('Auto-refreshes every 15 minutes', $content);
|
||||
$this->assertStringContainsString('Auto-refreshes every minute', $content);
|
||||
$this->assertStringContainsString('Last dashboard refresh:', $content);
|
||||
$this->assertStringContainsString('$dashboardLastRefreshedAt', $content);
|
||||
$this->assertStringContainsString('data-stat="last-refresh"', $content);
|
||||
@@ -47,8 +47,7 @@ class AdminDashboardAutoRefreshTest extends TestCase
|
||||
|
||||
$content = file_get_contents($scriptPath);
|
||||
|
||||
// Auto-refresh interval is preserved.
|
||||
$this->assertStringContainsString('15 * 60 * 1000', $content);
|
||||
$this->assertStringContainsString('60 * 1000', $content);
|
||||
// The fetch hits the cached JSON endpoint.
|
||||
$this->assertStringContainsString('this.$el.dataset.dataUrl', $content);
|
||||
// Each tick re-renders headline tiles + registration status + the
|
||||
|
||||
@@ -10,8 +10,7 @@ use Tests\TestCase;
|
||||
* Locks in the "(7d) = last 7 calendar days inclusive of today" semantics
|
||||
* and the source layering used by the headline summary tiles:
|
||||
*
|
||||
* - Closed days come from `user_activity_stats`.
|
||||
* - Today's closed hours come from `user_activity_stats_hourly`.
|
||||
* - Every completed hour comes from `user_activity_stats_hourly`.
|
||||
* - The current in-progress hour comes from the live `user_downloads` /
|
||||
* `user_requests` tables (which are pruned hourly to ~24h).
|
||||
*
|
||||
@@ -21,7 +20,7 @@ use Tests\TestCase;
|
||||
*/
|
||||
class AdminDashboardSummaryWindowTest extends TestCase
|
||||
{
|
||||
public function test_summary_window_layers_daily_hourly_and_live_sources(): void
|
||||
public function test_summary_window_layers_hourly_and_live_sources(): void
|
||||
{
|
||||
$servicePath = app_path('Services/UserStatsService.php');
|
||||
|
||||
@@ -36,18 +35,16 @@ class AdminDashboardSummaryWindowTest extends TestCase
|
||||
'Summary window must be the last 7 calendar days inclusive of today.'
|
||||
);
|
||||
|
||||
// Closed days from the daily aggregate, strictly before today.
|
||||
$this->assertStringContainsString('UserActivityStat::query()', $content);
|
||||
// Every completed hour in the seven-day window comes from the retained
|
||||
// hourly aggregate, including completed hours from today.
|
||||
$this->assertStringContainsString(
|
||||
"->where('stat_date', '>=', \$weekStart->format('Y-m-d'))",
|
||||
"->where('stat_hour', '>=', \$weekStart->format('Y-m-d H:00:00'))",
|
||||
$content
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
"->where('stat_date', '<', \$today->format('Y-m-d'))",
|
||||
"->where('stat_hour', '<', \$currentHourStart->format('Y-m-d H:00:00'))",
|
||||
$content
|
||||
);
|
||||
|
||||
// Today's closed hours from the hourly aggregate.
|
||||
$this->assertStringContainsString(
|
||||
"DB::table('user_activity_stats_hourly')",
|
||||
$content
|
||||
@@ -67,13 +64,13 @@ class AdminDashboardSummaryWindowTest extends TestCase
|
||||
$content
|
||||
);
|
||||
|
||||
// Today and week totals must combine all three sources.
|
||||
// Today and week totals combine closed hourly data with live data.
|
||||
$this->assertStringContainsString(
|
||||
"'downloads_today' => \$downloadsToday,",
|
||||
$content
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
"'downloads_week' => (int) (\$historical->downloads ?? 0) + \$downloadsToday,",
|
||||
"'downloads_week' => (int) (\$weekClosed->downloads ?? 0) + \$downloadsCurrentHour,",
|
||||
$content
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
@@ -81,7 +78,7 @@ class AdminDashboardSummaryWindowTest extends TestCase
|
||||
$content
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
"'api_hits_week' => (int) (\$historical->api_hits ?? 0) + \$apiHitsToday,",
|
||||
"'api_hits_week' => (int) (\$weekClosed->api_hits ?? 0) + \$apiHitsCurrentHour,",
|
||||
$content
|
||||
);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Jobs\RecordApiUsage;
|
||||
use App\Jobs\UpdateUserApiAccess;
|
||||
use App\Models\User;
|
||||
use App\Services\Api\ApiUsageService;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
@@ -44,7 +45,7 @@ final class ApiUsageServiceTest extends TestCase
|
||||
DB::table('users')->insert(['id' => 1]);
|
||||
}
|
||||
|
||||
public function test_async_record_dispatches_id_only_audit_job(): void
|
||||
public function test_async_record_persists_usage_before_dispatching_metadata_update(): void
|
||||
{
|
||||
Queue::fake();
|
||||
config(['nntmux.api.async_audit' => true]);
|
||||
@@ -54,12 +55,16 @@ final class ApiUsageServiceTest extends TestCase
|
||||
|
||||
(new ApiUsageService)->record($user, $request);
|
||||
|
||||
Queue::assertPushed(RecordApiUsage::class, static fn (RecordApiUsage $job): bool => $job->userId === 1 && $job->requestUri === '/api/v2/search?api_token=secret&id=test'
|
||||
Queue::assertPushed(UpdateUserApiAccess::class, static fn (UpdateUserApiAccess $job): bool => $job->userId === 1
|
||||
&& $job->ip === '127.0.0.1'
|
||||
);
|
||||
$this->assertSame(0, DB::table('user_requests')->count());
|
||||
$this->assertDatabaseHas('user_requests', [
|
||||
'users_id' => 1,
|
||||
'request' => '/api/v2/search?api_token=secret&id=test',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_audit_job_persists_each_request_and_coalesces_user_updates(): void
|
||||
public function test_legacy_audit_job_persists_queued_requests_and_coalesces_user_updates(): void
|
||||
{
|
||||
config(['nntmux.api.access_update_interval' => 60]);
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\UserActivityStat;
|
||||
use App\Services\UserStatsService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class UserActivityStatsTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'database.default' => 'sqlite',
|
||||
'database.connections.sqlite.database' => ':memory:',
|
||||
'cache.default' => 'array',
|
||||
]);
|
||||
DB::purge();
|
||||
DB::reconnect();
|
||||
Cache::flush();
|
||||
Carbon::setTestNow('2026-07-20 14:37:00');
|
||||
|
||||
Schema::create('users', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->dateTime('deleted_at')->nullable();
|
||||
});
|
||||
Schema::create('settings', function (Blueprint $table): void {
|
||||
$table->string('name')->primary();
|
||||
$table->text('value')->nullable();
|
||||
});
|
||||
Schema::create('user_requests', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('users_id');
|
||||
$table->text('request');
|
||||
$table->dateTime('timestamp');
|
||||
});
|
||||
Schema::create('user_downloads', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('users_id');
|
||||
$table->dateTime('timestamp');
|
||||
});
|
||||
Schema::create('user_activity_stats', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->date('stat_date')->unique();
|
||||
$table->integer('downloads_count')->default(0);
|
||||
$table->integer('api_hits_count')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('user_activity_stats_hourly', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->dateTime('stat_hour')->unique();
|
||||
$table->integer('downloads_count')->default(0);
|
||||
$table->integer('api_hits_count')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
DB::table('users')->insert(['id' => 1]);
|
||||
DB::table('settings')->insert([
|
||||
['name' => 'categorizeforeign', 'value' => '0'],
|
||||
['name' => 'catwebdl', 'value' => '0'],
|
||||
['name' => 'delaytime', 'value' => '0'],
|
||||
['name' => 'innerfileblacklist', 'value' => ''],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Carbon::setTestNow();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_summary_and_charts_combine_hourly_api_and_rss_hits_with_the_live_hour(): void
|
||||
{
|
||||
$this->insertHourly('2026-07-14 09:00:00', 3, 7);
|
||||
$this->insertHourly('2026-07-20 13:00:00', 2, 4);
|
||||
|
||||
DB::table('user_activity_stats')->insert([
|
||||
'stat_date' => '2026-07-19',
|
||||
'downloads_count' => 999,
|
||||
'api_hits_count' => 999,
|
||||
]);
|
||||
DB::table('user_requests')->insert([
|
||||
['users_id' => 1, 'request' => '/api/v2/search?id=one', 'timestamp' => '2026-07-20 14:37:00'],
|
||||
['users_id' => 1, 'request' => '/rss/full-feed?api_token=test', 'timestamp' => '2026-07-20 14:37:30'],
|
||||
]);
|
||||
DB::table('user_downloads')->insert([
|
||||
'users_id' => 1,
|
||||
'timestamp' => '2026-07-20 14:36:00',
|
||||
]);
|
||||
|
||||
$service = new UserStatsService;
|
||||
$summary = $service->getSummaryStats();
|
||||
|
||||
$this->assertSame(6, $summary['api_hits_today']);
|
||||
$this->assertSame(13, $summary['api_hits_week']);
|
||||
$this->assertSame(3, $summary['downloads_today']);
|
||||
$this->assertSame(6, $summary['downloads_week']);
|
||||
|
||||
$hourly = $service->getApiHitsPerHour(2);
|
||||
$this->assertSame([4, 2], array_column($hourly, 'count'));
|
||||
|
||||
$daily = $service->getApiHitsPerDay(7);
|
||||
$this->assertSame(7, $daily[0]['count']);
|
||||
$this->assertSame(6, $daily[6]['count']);
|
||||
|
||||
$perMinute = $service->getApiHitsPerMinute(60);
|
||||
$this->assertSame(2, $perMinute[59]['count']);
|
||||
}
|
||||
|
||||
public function test_daily_rollup_remains_stable_after_raw_rows_are_pruned(): void
|
||||
{
|
||||
$this->insertHourly('2026-07-19 01:00:00', 2, 3);
|
||||
$this->insertHourly('2026-07-19 23:00:00', 4, 5);
|
||||
DB::table('user_requests')->insert([
|
||||
'users_id' => 1,
|
||||
'request' => '/api/v1/api?t=search',
|
||||
'timestamp' => '2026-07-19 23:30:00',
|
||||
]);
|
||||
|
||||
UserActivityStat::collectDailyStats('2026-07-19');
|
||||
DB::table('user_requests')->delete();
|
||||
UserActivityStat::collectDailyStats('2026-07-19');
|
||||
|
||||
$daily = DB::table('user_activity_stats')->where('stat_date', '2026-07-19')->first();
|
||||
$this->assertSame(6, $daily->downloads_count);
|
||||
$this->assertSame(8, $daily->api_hits_count);
|
||||
}
|
||||
|
||||
public function test_forced_daily_backfill_rebuilds_retained_history_from_hourly_totals(): void
|
||||
{
|
||||
$this->insertHourly('2026-07-19 10:00:00', 11, 17);
|
||||
DB::table('user_activity_stats')->insert([
|
||||
'stat_date' => '2026-07-19',
|
||||
'downloads_count' => 1,
|
||||
'api_hits_count' => 1,
|
||||
]);
|
||||
|
||||
$this->artisan('nntmux:backfill-user-activity-stats', [
|
||||
'--type' => 'daily',
|
||||
'--days' => 2,
|
||||
'--force' => true,
|
||||
])->assertSuccessful();
|
||||
|
||||
$daily = DB::table('user_activity_stats')->where('stat_date', '2026-07-19')->first();
|
||||
$this->assertSame(11, $daily->downloads_count);
|
||||
$this->assertSame(17, $daily->api_hits_count);
|
||||
}
|
||||
|
||||
private function insertHourly(string $hour, int $downloads, int $apiHits): void
|
||||
{
|
||||
DB::table('user_activity_stats_hourly')->insert([
|
||||
'stat_hour' => $hour,
|
||||
'downloads_count' => $downloads,
|
||||
'api_hits_count' => $apiHits,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user