use Mojo::Base -strict; use Test2::V0; use FindBin; use lib "${FindBin::Bin}/../lib/"; use lib "${FindBin::Bin}/lib/"; use Dashboard::Test qw(test_app test_empty_app); my $t = test_app(); subtest 'insights page renders with all sections' => sub { $t->get_ok('/insights') ->status_is(200) ->text_is( 'title', 'FAPG DAQ Insights' ) ->element_exists('.navbar-nav a.nav-link.active[href="/insights"]') ->element_exists_not('[data-home-masthead]') ->element_exists_not('[data-home-koan-card]') ->element_exists( '[data-mobile-navigation] a.active[href="/insights"][aria-current="page"]' ) ->element_exists( 'main section[data-controls-panel][aria-labelledby="insights-controls-title"]' ) ->text_is( '#insights-controls-title', 'Insights controls' ) ->element_exists( '#insights-controls .graph-control[data-default-timeframe="day"]') ->element_exists('[data-default-timeframe="day"]'); }; subtest 'insights page has all chart canvases' => sub { $t->get_ok('/insights') ->status_is(200) ->element_exists('canvas#chart-stability') ->element_exists('canvas#chart-derivatives') ->element_exists('canvas#chart-diurnal') ->element_exists('canvas#chart-overlay-do-orp') ->element_exists('canvas#chart-ph-do-lag'); }; subtest 'diurnal API returns structured response' => sub { $t->get_ok('/api/insights/diurnal/do') ->status_is(200) ->json_has('/probe') ->json_is( '/probe' => 'do' ) ->json_has('/hours') ->json_has('/traces') ->json_has('/mean'); }; subtest 'diurnal API with invalid probe' => sub { $t->get_ok('/api/insights/diurnal/nope') ->status_is(404) ->json_has('/error'); }; subtest 'diurnal API accepts days param' => sub { $t->get_ok('/api/insights/diurnal/ph?days=3') ->status_is(200) ->json_is( '/days' => 3 ) ->json_is( '/probe' => 'ph' ); }; subtest 'derivatives API returns all probes' => sub { $t->get_ok('/api/insights/derivatives') ->status_is(200) ->json_has('/probes') ->json_has('/probes/ph') ->json_has('/probes/do') ->json_has('/probes/orp') ->json_has('/probes/ec') ->json_is( '/hours' => 48 ); }; subtest 'derivatives API accepts hours param' => sub { $t->get_ok('/api/insights/derivatives?hours=24') ->status_is(200) ->json_is( '/hours' => 24 ); }; subtest 'stability API returns points array' => sub { $t->get_ok('/api/insights/stability') ->status_is(200) ->json_has('/points') ->json_is( '/hours' => 168 ) ->json_is( '/window' => 6 ); }; subtest 'stability API accepts hours param' => sub { $t->get_ok('/api/insights/stability?hours=48') ->status_is(200) ->json_is( '/hours' => 48 ); }; subtest 'empty database insights page' => sub { my $empty = test_empty_app(); $empty->get_ok('/insights') ->status_is(200) ->element_exists('canvas#chart-stability'); }; subtest 'insights APIs with no recent data' => sub { # test_app has data from 2026-07-05, outside default time windows # so these return empty/minimal results gracefully $t->get_ok('/api/insights/stability?hours=1') ->status_is(200) ->json_is( '/points' => [] ); $t->get_ok('/api/insights/derivatives?hours=1') ->status_is(200) ->json_has('/probes'); $t->get_ok('/api/insights/diurnal/do?days=1') ->status_is(200) ->json_is( '/traces' => [] ); }; subtest 'insights APIs with matching data' => sub { # Seed recent hourly rollups to exercise computation paths my $now = time; for my $offset ( 1 .. 12 ) { my $epoch = ( int( $now / 3600 ) - $offset ) * 3600; for my $probe (qw(ph do orp ec)) { my %base = ( ph => 7.0, do => 8.0, orp => 350, ec => 800 ); my $val = $base{$probe} + ( $offset * 0.1 ); $t->app->sqlite->db->query( q{INSERT OR IGNORE INTO reading_rollups (probe, bucket_seconds, bucket_epoch, sample_count, value_total, value_min, value_max, first_sample_epoch, last_sample_epoch, max_gap_seconds) VALUES (?, 3600, ?, 10, ?, ?, ?, ?, ?, 0)}, $probe, $epoch, $val * 10, $val - 0.1, $val + 0.1, $epoch, $epoch + 3000, ); } } $t->get_ok('/api/insights/stability?hours=24') ->status_is(200) ->json_has('/points'); $t->get_ok('/api/insights/derivatives?hours=24') ->status_is(200) ->json_has('/probes/ph'); my $derivs = $t->tx->res->json->{probes}; ok @{ $derivs->{ph} } > 0, 'derivatives returns rate points for ph'; $t->get_ok('/api/insights/diurnal/do?days=1') ->status_is(200) ->json_has('/traces'); my $diurnal = $t->tx->res->json; ok @{ $diurnal->{traces} } > 0, 'diurnal returns traces with recent data'; }; done_testing;