From a254b98e6828864f08d45b388e1be613fc8e0c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ersin=20KO=C3=87?= Date: Fri, 11 Sep 2026 19:32:56 +0300 Subject: [PATCH 1/2] fix(dashboard): trend grafigi ay sonunda 7-11 aya dusuyordu Kok neden: strtotime('-N month') gun tasmasini kirmaz. Ayin 29-31'inde hedef ayda o gun yoksa sonuc bir SONRAKI aya tasiyor; ayni 'Y-m' anahtari iki kez uretiliyor ve api/dashboard_charts.php'deki 12 aylik trend penceresi 7-11 aylik map'e cokuyordu. Kayip aylarin riskleri trend graginde hic sayilmiyor, SQL penceresinin alt siniri (:since) da bir ay geriden geliyordu. Cozum: ay aritmetigi ayin 1'ine sabitlenen recent_months() yardimcisi (includes/functions.php) eklendi; uc nokta bu yardimciyi kullaniyor. 1. gunden cikarilan ay asla tasamaz. Kanit: .temp_files altindaki proof-script oncesinde 11 prob gununden 9'unda FAIL (buckets=7, since kayik), sonrasi PASS. Dayanikli regresyon testi tools/trend_months_test.php olarak eklendi (22 kontrol: ay sonu gunleri, artik yil, yil siniri, count sinirlari, uc nokta baglantisi; DB gerektirmez, php tools/trend_months_test.php ile calisir). --- api/dashboard_charts.php | 10 +-- includes/functions.php | 30 ++++++++ tools/trend_months_test.php | 133 ++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 tools/trend_months_test.php diff --git a/api/dashboard_charts.php b/api/dashboard_charts.php index 6a09946..a5e8b71 100644 --- a/api/dashboard_charts.php +++ b/api/dashboard_charts.php @@ -37,11 +37,11 @@ /* tek eksende gosterilebilir). */ /* ------------------------------------------------------------------ */ -$months = []; -for ($i = 11; $i >= 0; $i--) { - $months[date('Y-m', strtotime("-{$i} month"))] = ['opened' => 0, 'closed' => 0]; -} -$since = date('Y-m-01', strtotime('-11 month')); +// Takvim ayi penceresi ayin 1'ine sabitlenerek kurulur (bkz. recent_months): +// strtotime('-N month') ay sonu gunlerinde bir sonraki aya tasiyordu. +$monthKeys = recent_months(12); +$months = array_fill_keys($monthKeys, ['opened' => 0, 'closed' => 0]); +$since = $monthKeys[0] . '-01'; $rows = $pdo->prepare( "SELECT DATE_FORMAT(created_at, '%Y-%m') AS ay, COUNT(*) AS adet diff --git a/includes/functions.php b/includes/functions.php index 92f9613..b0a854b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -216,6 +216,36 @@ function days_until(?string $date): ?int return (int)floor(($t - strtotime(date('Y-m-d'))) / 86400); } +/** + * Son $count takvim ayinin 'Y-m' anahtarlarini eskiden yeniye sirali dondurur. + * Pencerenin alt siniri (SQL :since degeri) ilk anahtarin '-01' ekidir. + * + * NEDEN AYRI BIR FONKSIYON: strtotime('-N month') gun tasmasini kirmaz; + * ayin 29-31'inde hedef ayda o gun yoksa sonuc bir SONRAKI aya taser. + * Dashboard trend penceresi (api/dashboard_charts.php) bu yuzden ay sonu + * isteklerinde 12 yerine 7-11 ay uretiyor, kayip aylarin riskleri hic + * sayilmiyordu. Burada ay aritmetigi her zaman ayin 1'ine sabitlenir; + * 1. gunden cikarilan ay asla tasmaz. + * + * @param int $count Kac ay dondurecek (1 = yalnizca bulunulan ay). + * @param int|null $baseTs Pencerenin bittigi an (null = simdi). + * @return list 'YYYY-MM' anahtarlari, eskiden yeniye. + */ +function recent_months(int $count, ?int $baseTs = null): array +{ + if ($count < 1) { + return []; + } + $anchor = (new DateTimeImmutable(date('Y-m-d H:i:s', $baseTs ?? time()))) + ->modify('first day of this month midnight'); + + $out = []; + for ($i = $count - 1; $i >= 0; $i--) { + $out[] = $anchor->modify("-{$i} months")->format('Y-m'); + } + return $out; +} + function str_limit(?string $text, int $limit = 80): string { $text = trim((string)$text); diff --git a/tools/trend_months_test.php b/tools/trend_months_test.php new file mode 100644 index 0000000..38aad6f --- /dev/null +++ b/tools/trend_months_test.php @@ -0,0 +1,133 @@ += 0; $k--) { + $out[] = date('Y-m', mktime(1, 1, 1, $m - $k, 1, $y)); + } + return $out; +} + +echo "\n=============== Trend Month Window Regression Test ==============="; + +/* ------------------------------------------------------------------ */ +section('1. Ay sonu gunlerinde pencere (regresyon)'); +/* ------------------------------------------------------------------ */ + +// Hatali davranisin kanitlandigi gunler: 29-31 (hedef ay kisa ise tasma). +$probes = [ + '2026-05-31', '2026-03-31', '2026-08-31', '2025-12-31', + '2026-01-31', '2026-01-30', '2024-02-29', '2026-02-28', + '2026-10-31', '2026-04-30', '2025-11-30', '2026-12-31', +]; + +foreach ($probes as $d) { + $bts = strtotime($d . ' 12:00:00'); + $got = recent_months(12, $bts); + check( + "12 ay: {$d}", + $got === expected_months($bts, 12) && count(array_unique($got)) === 12, + count($got) . ' ay' + ); +} + +/* ------------------------------------------------------------------ */ +section('2. Sinir ve ikincil dallar'); +/* ------------------------------------------------------------------ */ + +$now = time(); +check('bugun (varsayilan taban): 12 ay', + recent_months(12) === expected_months($now, 12)); +check('taban null acik gecilirse ayni sonuc', + recent_months(12, null) === recent_months(12)); +check('count=1 yalnizca bulunulan ay', + recent_months(1, $now) === [date('Y-m', $now)]); +check('count=0 bos liste', recent_months(0, $now) === []); +check('count negatif bos liste', recent_months(-3, $now) === []); +check('count=13 yil sinirini asar', + recent_months(13, strtotime('2026-01-15 12:00:00')) + === expected_months(strtotime('2026-01-15 12:00:00'), 13)); +check('anahtarlar eskiden yeniye sirali', + recent_months(12, $now) === array_values(array_sort(recent_months(12, $now)))); + +/* SQL :since degeri: en eski ayin 1'i (api/dashboard_charts.php tuketimi) */ +$keys = recent_months(12, strtotime('2026-05-31 12:00:00')); +check('since = en eski ayin 1\'i (2026-05-31 taban)', + $keys[0] . '-01' === '2025-06-01', $keys[0] . '-01'); + +/* ------------------------------------------------------------------ */ +section('3. Uc nokta baglantisi'); +/* ------------------------------------------------------------------ */ + +$src = (string)file_get_contents(__DIR__ . '/../api/dashboard_charts.php'); +check('api/dashboard_charts.php recent_months() kullaniyor', + str_contains($src, 'recent_months(12)')); +check('bozuk strtotime("-N month") ifadesi kaldirildi', + !str_contains($src, 'strtotime("-{')); + +/* ------------------------------------------------------------------ */ + +echo "\n" . str_repeat('-', 72) . "\n"; +printf("Sonuc: %d OK, %d FAIL\n", $PASS, $FAIL); +if ($FAIL > 0) { + echo "TREND MONTH WINDOW TEST: FAIL\n"; + exit(1); +} +echo "TREND MONTH WINDOW TEST: PASS\n"; +exit(0); + +/** Kucukten buyuge siralar (strcmp) — 'Y-m' anahtarlari icin yeterli. */ +function array_sort(array $a): array +{ + usort($a, static fn(string $x, string $y) => strcmp($x, $y)); + return $a; +} From 331929cb011b0caca9c212d3ca253f9eeff76808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ersin=20KO=C3=87?= Date: Fri, 11 Sep 2026 19:41:19 +0300 Subject: [PATCH 2/2] fix(reports): yonetici ozeti Son 6 Ay tablosu ay sonunda 3-5 aya dusuyordu Kok neden (PR #1 ile ailesi): strtotime('-N month') gun tasmasini kirmaz. Ayin 29-31'inde hedef ayda o gun yoksa sonuc bir SONRAKI aya tasiyor; reports/executive_summary.php'deki "Son 6 Ay" trend map'i yinelenen 'Y-m' anahtarlarindan 3-5 aya kadar kuculuyordu (2026-05-31 tabaninda 2026-02 ve 2026-04 tamamen kayboluyordu) ve SQL penceresinin alt siniri Temmuz 29-31'de bir ay geride kaliyordu (2026-03-01 yerine 2026-02-01). Cozum: pencere ayin 1'ine sabitlenen recent_months(6) ile kuruluyor (PR #1'de eklenen yardimci); :since degeri en eski ayin 1'inden turetiliyor. Kanit: round-owned proof-script oncesinde 12 prob gununden 9'unda FAIL (rows=3-5, since kayik), sonrasi PASS. Dayanikli regresyon testi tools/trend_months_test.php genisletildi (25 kontrol: 6 aylik pencere + yonetici ozeti baglanti guvenligi; DB gerektirmez). Not: bu dal PR #1 (fix/dashboard-trend-month-window) uzerine istiflidir; trend_months_test.php recent_months()'a baglidir. PR #1 birlestiginde base otomatik olarak main'e doner. --- reports/executive_summary.php | 12 ++++++------ tools/trend_months_test.php | 24 +++++++++++++++++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/reports/executive_summary.php b/reports/executive_summary.php index f23ee2d..b18584e 100644 --- a/reports/executive_summary.php +++ b/reports/executive_summary.php @@ -85,12 +85,12 @@ LIMIT 8" )->fetchAll(); -/* Son 6 ay: açılan ve kapanan */ -$trend = []; -for ($i = 5; $i >= 0; $i--) { - $trend[date('Y-m', strtotime("-{$i} month"))] = ['acilan' => 0, 'kapanan' => 0]; -} -$since = date('Y-m-01', strtotime('-5 month')); +/* Son 6 ay: açılan ve kapanan — takvim ayı penceresi ayın 1'ine + sabitlenerek kurulur (bkz. recent_months): strtotime('-N month') + ay sonu günlerinde bir sonraki aya taşıyordu. */ +$monthKeys = recent_months(6); +$trend = array_fill_keys($monthKeys, ['acilan' => 0, 'kapanan' => 0]); +$since = $monthKeys[0] . '-01'; $q = db()->prepare("SELECT DATE_FORMAT(created_at,'%Y-%m') AS ay, COUNT(*) AS n FROM risks WHERE deleted_at IS NULL AND created_at >= :s GROUP BY ay"); diff --git a/tools/trend_months_test.php b/tools/trend_months_test.php index 38aad6f..5ea9d5a 100644 --- a/tools/trend_months_test.php +++ b/tools/trend_months_test.php @@ -2,18 +2,23 @@ declare(strict_types=1); /** - * RiskOps - Dashboard trend ay penceresi regresyon testi + * RiskOps - Ay bazli trend pencereleri regresyon testi * Calistirma: php tools/trend_months_test.php * * Bagimlilik YOK: veritabani ve bootstrap gerekmez, yalnizca * includes/functions.php icindeki recent_months() test edilir. * - * REGRESYON ARKAPLANI: pencere bir zamanlar + * Kapsanan tuketiciler: + * api/dashboard_charts.php (12 aylik trend penceresi) + * reports/executive_summary.php (6 aylik "Son 6 Ay" penceresi) + * + * REGRESYON ARKAPLANI: pencereler bir zamanlar * date('Y-m', strtotime("-{$i} month")) * ile kuruluyordu. strtotime ay aritmetiginde gun tasmasini kirmaz; * ayin 29-31'inde hedef ayda o gun yoksa sonuc bir SONRAKI aya tasiyor, - * ayni 'Y-m' anahtari iki kez uretiliyor ve 12 aylik pencere 7-11 aya - * dusuyordu (kayip aylarin riskleri trend grafiginde hic sayilmiyordu). + * ayni 'Y-m' anahtari iki kez uretiliyor ve pencere kuculuyordu + * (dashboard 7-11 aya, yonetici ozeti 3-5 aya dusuyordu; kayip + * aylarin riskleri hic sayilmiyordu). */ if (PHP_SAPI !== 'cli') { @@ -96,6 +101,9 @@ function expected_months(int $baseTs, int $count): array check('count=13 yil sinirini asar', recent_months(13, strtotime('2026-01-15 12:00:00')) === expected_months(strtotime('2026-01-15 12:00:00'), 13)); +check('count=6 yonetici ozeti penceresi (2026-05-31 taban)', + recent_months(6, strtotime('2026-05-31 12:00:00')) + === expected_months(strtotime('2026-05-31 12:00:00'), 6)); check('anahtarlar eskiden yeniye sirali', recent_months(12, $now) === array_values(array_sort(recent_months(12, $now)))); @@ -111,9 +119,15 @@ function expected_months(int $baseTs, int $count): array $src = (string)file_get_contents(__DIR__ . '/../api/dashboard_charts.php'); check('api/dashboard_charts.php recent_months() kullaniyor', str_contains($src, 'recent_months(12)')); -check('bozuk strtotime("-N month") ifadesi kaldirildi', +check('dashboard bozuk strtotime("-N month") ifadesi kaldirildi', !str_contains($src, 'strtotime("-{')); +$srcExec = (string)file_get_contents(__DIR__ . '/../reports/executive_summary.php'); +check('reports/executive_summary.php recent_months() kullaniyor', + str_contains($srcExec, 'recent_months(6)')); +check('yonetici ozeti bozuk strtotime ay ifadeleri kaldirildi', + !str_contains($srcExec, 'strtotime("-{') && !str_contains($srcExec, "strtotime('-5 month')")); + /* ------------------------------------------------------------------ */ echo "\n" . str_repeat('-', 72) . "\n";