diff --git a/app/Http/Controllers/ItemController.php b/app/Http/Controllers/ItemController.php index cd5492b1e..46a8ec238 100644 --- a/app/Http/Controllers/ItemController.php +++ b/app/Http/Controllers/ItemController.php @@ -587,18 +587,46 @@ public function websitelookup($url): StreamInterface } /** - * @param $id - * @return void + * Return live stats for an enhanced application tile. + * + * Always responds with HTTP 200 and valid JSON so the frontend refresh + * loop (liveStatRefresh.js) keeps re-queueing the tile. On any failure we + * degrade gracefully to an inactive, empty tile instead of a 500. + * + * @param int|string $id + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response */ public function getStats($id) { + $graceful = response()->json(['status' => 'inactive', 'html' => '']); + $item = Item::find($id); + if ($item === null) { + return $graceful; + } + + // Non-enhanced items (or stale records) have no live-stats class. + if (empty($item->class)) { + return $graceful; + } + + try { + $config = $item->getconfig(); + + // Guard against a stale/renamed class string from the remote apps repo. + if (! class_exists($item->class)) { + return $graceful; + } - $config = $item->getconfig(); - if (isset($item->class)) { $application = new $item->class; $application->config = $config; - echo $application->livestats(); + + // livestats() returns a JSON string; return it verbatim (no re-encoding). + return response($application->livestats()); + } catch (\Throwable $e) { + Log::error('getStats failed for item '.$id.' ('.$item->class.'): '.$e->getMessage()); + + return $graceful; } } diff --git a/tests/Feature/GetStatsTest.php b/tests/Feature/GetStatsTest.php new file mode 100644 index 000000000..de9d7bed0 --- /dev/null +++ b/tests/Feature/GetStatsTest.php @@ -0,0 +1,87 @@ + 'active', 'html' => 'ok']); + } +} + +class GetStatsTest extends TestCase +{ + use RefreshDatabase; + + public function test_missing_item_id_does_not_500(): void + { + $response = $this->get('get_stats/999999'); + + $response->assertStatus(200); + $response->assertJson(['status' => 'inactive', 'html' => '']); + } + + public function test_throwing_app_degrades_gracefully(): void + { + $item = Item::factory()->create([ + 'class' => ThrowingStatApp::class, + ]); + + $response = $this->get('get_stats/'.$item->id); + + $response->assertStatus(200); + $response->assertJson(['status' => 'inactive', 'html' => '']); + } + + public function test_item_with_no_class_degrades_gracefully(): void + { + $item = Item::factory()->create([ + 'class' => null, + ]); + + $response = $this->get('get_stats/'.$item->id); + + $response->assertStatus(200); + $response->assertJson(['status' => 'inactive', 'html' => '']); + } + + public function test_happy_path_returns_livestats_output_verbatim(): void + { + $item = Item::factory()->create([ + 'class' => HappyStatApp::class, + ]); + + $expected = json_encode(['status' => 'active', 'html' => 'ok']); + + $response = $this->get('get_stats/'.$item->id); + + $response->assertStatus(200); + $this->assertSame($expected, $response->getContent()); + $response->assertJson(['status' => 'active', 'html' => 'ok']); + } +}