feat(Метрики МП): monthly cumulative model + YoY trend sparklines
- updater: group by report_period_id (YYYYmm), current month capped to yesterday symmetrically for both years; per-year cumulative reset each Jan; adsl CTE now date-bounded for equal-period comparison - JSON adds months/month_labels + per-metric cur_cum/prev_cum - page: per-card cumulative sparkline (2025 dashed vs 2026 solid) + legend
This commit is contained in:
parent
4acad9a25c
commit
5d82f7b7f3
@ -94,14 +94,20 @@ select.txt-input { cursor: pointer; background: #fff; }
|
|||||||
.badge.new { background: var(--color-purple-bg); color: var(--color-purple); }
|
.badge.new { background: var(--color-purple-bg); color: var(--color-purple); }
|
||||||
.card-value { font-size: 30px; font-weight: 700; font-family: var(--font-mono); line-height: 1; }
|
.card-value { font-size: 30px; font-weight: 700; font-family: var(--font-mono); line-height: 1; }
|
||||||
.card-value-year { font-size: 11px; color: var(--color-text-secondary); font-weight: 600; margin-left: 2px; }
|
.card-value-year { font-size: 11px; color: var(--color-text-secondary); font-weight: 600; margin-left: 2px; }
|
||||||
.cmp { margin-top: 14px; display: flex; flex-direction: column; gap: 7px; }
|
.card-sub { font-size: 11px; color: var(--color-text-secondary); margin-top: 2px; }
|
||||||
.cmp-row { display: grid; grid-template-columns: 38px 1fr auto; align-items: center; gap: 8px; }
|
/* накопительный мини-график (нарастающим итогом по месяцам) */
|
||||||
.cmp-year { font-size: 11px; color: var(--color-text-secondary); font-weight: 600; font-family: var(--font-mono); }
|
.spark { width: 100%; height: 52px; display: block; margin-top: 12px; overflow: visible; }
|
||||||
.cmp-track { height: 8px; background: var(--color-bg); border-radius: 8px; overflow: hidden; }
|
.spark polyline { fill: none; stroke-width: 2; vector-effect: non-scaling-stroke; stroke-linecap: round; stroke-linejoin: round; }
|
||||||
.cmp-fill { height: 100%; border-radius: 8px; transition: width 0.6s ease; min-width: 2px; }
|
.spark .sp-cur { stroke: var(--color-brand); }
|
||||||
.cmp-fill.cur { background: var(--color-brand); }
|
.spark .sp-prev { stroke: #C7D2E8; stroke-dasharray: 3 3; }
|
||||||
.cmp-fill.prev { background: #C7D2E8; }
|
.spark .dot-cur { fill: var(--color-brand); }
|
||||||
.cmp-num { font-size: 11px; font-family: var(--font-mono); color: var(--color-text-secondary); white-space: nowrap; }
|
.spark-x { display: flex; justify-content: space-between; font-size: 9px; color: var(--color-text-secondary); font-family: var(--font-mono); margin-top: 2px; }
|
||||||
|
.legend { display: flex; gap: 14px; margin-top: 10px; font-size: 11px; }
|
||||||
|
.legend span { display: flex; align-items: center; gap: 5px; color: var(--color-text-secondary); }
|
||||||
|
.legend b { font-family: var(--font-mono); color: var(--color-text-primary); font-weight: 600; }
|
||||||
|
.legend .dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.legend .dot.cur { background: var(--color-brand); }
|
||||||
|
.legend .dot.prev { background: #C7D2E8; }
|
||||||
.empty-note { text-align: center; color: var(--color-text-secondary); padding: 40px; grid-column: 1 / -1; }
|
.empty-note { text-align: center; color: var(--color-text-secondary); padding: 40px; grid-column: 1 / -1; }
|
||||||
|
|
||||||
/* loading / error */
|
/* loading / error */
|
||||||
@ -132,7 +138,7 @@ select.txt-input { cursor: pointer; background: #fff; }
|
|||||||
<div class="header-logo">KT</div>
|
<div class="header-logo">KT</div>
|
||||||
<div>
|
<div>
|
||||||
<h1>📱 Метрики МП</h1>
|
<h1>📱 Метрики МП</h1>
|
||||||
<p>Использование функций мобильного приложения</p>
|
<p>Использование функций МП · нарастающим итогом, год к году</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
@ -168,7 +174,7 @@ const DATA_URL = 'app_metrics.json';
|
|||||||
|
|
||||||
// Резервные данные (на случай file:// или недоступности файла). Обновляются скриптом updater.
|
// Резервные данные (на случай file:// или недоступности файла). Обновляются скриптом updater.
|
||||||
const EMBEDDED_METRICS = /*__DATA__*/{
|
const EMBEDDED_METRICS = /*__DATA__*/{
|
||||||
"generated_at": "2026-06-16T17:30:19",
|
"generated_at": "2026-06-16T17:55:07",
|
||||||
"cur_year": 2026,
|
"cur_year": 2026,
|
||||||
"prev_year": 2025,
|
"prev_year": 2025,
|
||||||
"period_label": "с 1 января по 15 июня",
|
"period_label": "с 1 января по 15 июня",
|
||||||
@ -176,230 +182,695 @@ const EMBEDDED_METRICS = /*__DATA__*/{
|
|||||||
"start": "2026-01-01",
|
"start": "2026-01-01",
|
||||||
"end": "2026-06-15"
|
"end": "2026-06-15"
|
||||||
},
|
},
|
||||||
|
"months": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"month_labels": [
|
||||||
|
"Янв",
|
||||||
|
"Фев",
|
||||||
|
"Мар",
|
||||||
|
"Апр",
|
||||||
|
"Май",
|
||||||
|
"Июн"
|
||||||
|
],
|
||||||
|
"cumulative": true,
|
||||||
"metrics": [
|
"metrics": [
|
||||||
{
|
{
|
||||||
"key": "my_services",
|
"key": "my_services",
|
||||||
"label": "Мои услуги",
|
"label": "Мои услуги",
|
||||||
"prev": 1459571,
|
|
||||||
"cur": 1769470,
|
"cur": 1769470,
|
||||||
"growth": 0.21232197680003234,
|
"prev": 1464715,
|
||||||
"is_new": false
|
"growth": 0.2080643674708049,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
321989,
|
||||||
|
621597,
|
||||||
|
980877,
|
||||||
|
1327884,
|
||||||
|
1676590,
|
||||||
|
1769470
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
308264,
|
||||||
|
594530,
|
||||||
|
857453,
|
||||||
|
1108674,
|
||||||
|
1341165,
|
||||||
|
1464715
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "traffic",
|
"key": "traffic",
|
||||||
"label": "Детализация трафика",
|
"label": "Детализация трафика",
|
||||||
"prev": 1079736,
|
|
||||||
"cur": 1271563,
|
"cur": 1271563,
|
||||||
"growth": 0.17766102084213178,
|
"prev": 1083912,
|
||||||
"is_new": false
|
"growth": 0.17312383293108666,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
246604,
|
||||||
|
463349,
|
||||||
|
727262,
|
||||||
|
978019,
|
||||||
|
1179021,
|
||||||
|
1271563
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
214948,
|
||||||
|
425004,
|
||||||
|
634594,
|
||||||
|
827602,
|
||||||
|
1001634,
|
||||||
|
1083912
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "payments",
|
"key": "payments",
|
||||||
"label": "Платежи",
|
"label": "Платежи",
|
||||||
"prev": 553808,
|
|
||||||
"cur": 730185,
|
"cur": 730185,
|
||||||
"growth": 0.3184804119839367,
|
"prev": 555155,
|
||||||
"is_new": false
|
"growth": 0.31528131783015556,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
111523,
|
||||||
|
203960,
|
||||||
|
358189,
|
||||||
|
523887,
|
||||||
|
653394,
|
||||||
|
730185
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
120062,
|
||||||
|
232395,
|
||||||
|
331976,
|
||||||
|
427765,
|
||||||
|
512345,
|
||||||
|
555155
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "orders",
|
"key": "orders",
|
||||||
"label": "Заявки",
|
"label": "Заявки",
|
||||||
"prev": 635621,
|
|
||||||
"cur": 826255,
|
"cur": 826255,
|
||||||
"growth": 0.2999177182629271,
|
"prev": 638026,
|
||||||
"is_new": false
|
"growth": 0.2950177578970136,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
173075,
|
||||||
|
312944,
|
||||||
|
461361,
|
||||||
|
601607,
|
||||||
|
742766,
|
||||||
|
826255
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
153918,
|
||||||
|
301147,
|
||||||
|
409051,
|
||||||
|
500862,
|
||||||
|
587480,
|
||||||
|
638026
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "loyalty",
|
"key": "loyalty",
|
||||||
"label": "Лояльность",
|
"label": "Лояльность",
|
||||||
"prev": 464365,
|
|
||||||
"cur": 470969,
|
"cur": 470969,
|
||||||
"growth": 0.014221571393192856,
|
"prev": 465354,
|
||||||
"is_new": false
|
"growth": 0.012066083024965939,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
86429,
|
||||||
|
152587,
|
||||||
|
264979,
|
||||||
|
351819,
|
||||||
|
432695,
|
||||||
|
470969
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
88583,
|
||||||
|
196751,
|
||||||
|
284932,
|
||||||
|
371536,
|
||||||
|
431175,
|
||||||
|
465354
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "pay",
|
"key": "pay",
|
||||||
"label": "Оплата",
|
"label": "Оплата",
|
||||||
"prev": 302510,
|
|
||||||
"cur": 337103,
|
"cur": 337103,
|
||||||
"growth": 0.11435324452084229,
|
"prev": 303542,
|
||||||
"is_new": false
|
"growth": 0.11056460061540084,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
73119,
|
||||||
|
141842,
|
||||||
|
198550,
|
||||||
|
248838,
|
||||||
|
304803,
|
||||||
|
337103
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
50344,
|
||||||
|
101847,
|
||||||
|
150536,
|
||||||
|
205493,
|
||||||
|
269251,
|
||||||
|
303542
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "billing_detail",
|
"key": "billing_detail",
|
||||||
"label": "Детали счета",
|
"label": "Детали счета",
|
||||||
"prev": 358290,
|
|
||||||
"cur": 496915,
|
"cur": 496915,
|
||||||
"growth": 0.3869072539004717,
|
"prev": 358855,
|
||||||
"is_new": false
|
"growth": 0.38472363489431666,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
81176,
|
||||||
|
147137,
|
||||||
|
241789,
|
||||||
|
343223,
|
||||||
|
431096,
|
||||||
|
496915
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
62482,
|
||||||
|
134395,
|
||||||
|
200427,
|
||||||
|
260559,
|
||||||
|
315518,
|
||||||
|
358855
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "viktorina",
|
"key": "viktorina",
|
||||||
"label": "Викторина KT Club",
|
"label": "Викторина KT Club",
|
||||||
"prev": 298475,
|
|
||||||
"cur": 213879,
|
"cur": 213879,
|
||||||
|
"prev": 298475,
|
||||||
"growth": -0.28342742273222216,
|
"growth": -0.28342742273222216,
|
||||||
"is_new": false
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
118526,
|
||||||
|
213879,
|
||||||
|
213879,
|
||||||
|
213879
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
49414,
|
||||||
|
298475,
|
||||||
|
298475,
|
||||||
|
298475
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "partners",
|
"key": "partners",
|
||||||
"label": "Акции партнеров",
|
"label": "Акции партнеров",
|
||||||
"prev": 94639,
|
|
||||||
"cur": 197009,
|
"cur": 197009,
|
||||||
"growth": 1.08168936696288,
|
"prev": 94796,
|
||||||
"is_new": false
|
"growth": 1.0782416979619394,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
40566,
|
||||||
|
65001,
|
||||||
|
117646,
|
||||||
|
149938,
|
||||||
|
185887,
|
||||||
|
197009
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
27730,
|
||||||
|
49011,
|
||||||
|
65023,
|
||||||
|
75359,
|
||||||
|
89282,
|
||||||
|
94796
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "tv_plus",
|
"key": "tv_plus",
|
||||||
"label": "TV+",
|
"label": "TV+",
|
||||||
"prev": 95647,
|
|
||||||
"cur": 64104,
|
"cur": 64104,
|
||||||
"growth": -0.32978556567377965,
|
"prev": 95883,
|
||||||
"is_new": false
|
"growth": -0.33143518663370986,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
11947,
|
||||||
|
20939,
|
||||||
|
33969,
|
||||||
|
45372,
|
||||||
|
56323,
|
||||||
|
64104
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
24830,
|
||||||
|
44479,
|
||||||
|
61671,
|
||||||
|
76824,
|
||||||
|
90692,
|
||||||
|
95883
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "boosters",
|
"key": "boosters",
|
||||||
"label": "Бустеры",
|
"label": "Бустеры",
|
||||||
"prev": 53649,
|
|
||||||
"cur": 121065,
|
"cur": 121065,
|
||||||
"growth": 1.2566124252082984,
|
"prev": 54053,
|
||||||
"is_new": false
|
"growth": 1.2397461750504135,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
15882,
|
||||||
|
33486,
|
||||||
|
61530,
|
||||||
|
88553,
|
||||||
|
114321,
|
||||||
|
121065
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
7244,
|
||||||
|
12487,
|
||||||
|
18093,
|
||||||
|
31284,
|
||||||
|
49552,
|
||||||
|
54053
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "roaming",
|
"key": "roaming",
|
||||||
"label": "Роуминг",
|
"label": "Роуминг",
|
||||||
"prev": 39200,
|
|
||||||
"cur": 22160,
|
"cur": 22160,
|
||||||
"growth": -0.4346938775510204,
|
"prev": 39403,
|
||||||
"is_new": false
|
"growth": -0.43760627363398724,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
2924,
|
||||||
|
5494,
|
||||||
|
8974,
|
||||||
|
11894,
|
||||||
|
18915,
|
||||||
|
22160
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
6678,
|
||||||
|
13021,
|
||||||
|
19649,
|
||||||
|
27201,
|
||||||
|
35474,
|
||||||
|
39403
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "pereoform",
|
"key": "pereoform",
|
||||||
"label": "Переоформление",
|
"label": "Переоформление",
|
||||||
"prev": 23537,
|
|
||||||
"cur": 34570,
|
"cur": 34570,
|
||||||
"growth": 0.46875132769681777,
|
"prev": 23644,
|
||||||
"is_new": false
|
"growth": 0.46210455083742175,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
7556,
|
||||||
|
12691,
|
||||||
|
18718,
|
||||||
|
24710,
|
||||||
|
30986,
|
||||||
|
34570
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
4876,
|
||||||
|
9244,
|
||||||
|
13556,
|
||||||
|
17422,
|
||||||
|
21572,
|
||||||
|
23644
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "aitu_music",
|
"key": "aitu_music",
|
||||||
"label": "Aitu Music",
|
"label": "Aitu Music",
|
||||||
"prev": 0,
|
|
||||||
"cur": 8651,
|
"cur": 8651,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
1048,
|
||||||
|
4000,
|
||||||
|
5553,
|
||||||
|
7270,
|
||||||
|
8651
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "online_booking",
|
"key": "online_booking",
|
||||||
"label": "Онлайн очередь",
|
"label": "Онлайн очередь",
|
||||||
"prev": 5421,
|
|
||||||
"cur": 22144,
|
"cur": 22144,
|
||||||
"growth": 3.084855192768862,
|
"prev": 5449,
|
||||||
"is_new": false
|
"growth": 3.063864929344834,
|
||||||
|
"is_new": false,
|
||||||
|
"cur_cum": [
|
||||||
|
5636,
|
||||||
|
10095,
|
||||||
|
13892,
|
||||||
|
17262,
|
||||||
|
20514,
|
||||||
|
22144
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1848,
|
||||||
|
4407,
|
||||||
|
5449
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "my_docs",
|
"key": "my_docs",
|
||||||
"label": "Мои документы",
|
"label": "Мои документы",
|
||||||
"prev": 0,
|
|
||||||
"cur": 53376,
|
"cur": 53376,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
14980,
|
||||||
|
24658,
|
||||||
|
33590,
|
||||||
|
41765,
|
||||||
|
49552,
|
||||||
|
53376
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "dz_statement",
|
"key": "dz_statement",
|
||||||
"label": "Справка о ДЗ",
|
"label": "Справка о ДЗ",
|
||||||
"prev": 0,
|
|
||||||
"cur": 132795,
|
"cur": 132795,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
35454,
|
||||||
|
64672,
|
||||||
|
85004,
|
||||||
|
100574,
|
||||||
|
119910,
|
||||||
|
132795
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "new_boosters_roaming_kcell",
|
"key": "new_boosters_roaming_kcell",
|
||||||
"label": "Новая линейка бустеров и роумингов Кселл",
|
"label": "Новая линейка бустеров и роумингов Кселл",
|
||||||
"prev": 0,
|
|
||||||
"cur": 28626,
|
"cur": 28626,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
1927,
|
||||||
|
4523,
|
||||||
|
13318,
|
||||||
|
21056,
|
||||||
|
28239,
|
||||||
|
28626
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "adsl",
|
"key": "adsl",
|
||||||
"label": "ADSL отключение услуги",
|
"label": "ADSL отключение услуги",
|
||||||
"prev": 0,
|
|
||||||
"cur": 69,
|
"cur": 69,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
20,
|
||||||
|
33,
|
||||||
|
44,
|
||||||
|
55,
|
||||||
|
65,
|
||||||
|
69
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "law_and_order",
|
"key": "law_and_order",
|
||||||
"label": "Закон и порядок",
|
"label": "Закон и порядок",
|
||||||
"prev": 0,
|
|
||||||
"cur": 1555,
|
"cur": 1555,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
232,
|
||||||
|
671,
|
||||||
|
1034,
|
||||||
|
1377,
|
||||||
|
1555
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "acs",
|
"key": "acs",
|
||||||
"label": "ACS",
|
"label": "ACS",
|
||||||
"prev": 0,
|
|
||||||
"cur": 9154,
|
"cur": 9154,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
2329,
|
||||||
|
7499,
|
||||||
|
7800,
|
||||||
|
8174,
|
||||||
|
9154
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "kaspi_freedom_pay",
|
"key": "kaspi_freedom_pay",
|
||||||
"label": "Прием платежей через Freedom и Kaspi",
|
"label": "Прием платежей через Freedom и Kaspi",
|
||||||
"prev": 0,
|
|
||||||
"cur": 61481,
|
"cur": 61481,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
8427,
|
||||||
|
24816,
|
||||||
|
39893,
|
||||||
|
53444,
|
||||||
|
61481
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "csat",
|
"key": "csat",
|
||||||
"label": "CSAT",
|
"label": "CSAT",
|
||||||
"prev": 0,
|
|
||||||
"cur": 2486,
|
"cur": 2486,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2486
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "multicustomer",
|
"key": "multicustomer",
|
||||||
"label": "Мультикастомер",
|
"label": "Мультикастомер",
|
||||||
"prev": 0,
|
|
||||||
"cur": 164,
|
"cur": 164,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
99,
|
||||||
|
164
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "tv_plus_setup",
|
"key": "tv_plus_setup",
|
||||||
"label": "Настройка TV+",
|
"label": "Настройка TV+",
|
||||||
"prev": 0,
|
|
||||||
"cur": 4545,
|
"cur": 4545,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2628,
|
||||||
|
4545
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "static_ip",
|
"key": "static_ip",
|
||||||
"label": "Статический IP",
|
"label": "Статический IP",
|
||||||
"prev": 0,
|
|
||||||
"cur": 108,
|
"cur": 108,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
35,
|
||||||
|
75,
|
||||||
|
108
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "turbo_button",
|
"key": "turbo_button",
|
||||||
"label": "Turbo кнопка",
|
"label": "Turbo кнопка",
|
||||||
"prev": 0,
|
|
||||||
"cur": 4312,
|
"cur": 4312,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
498,
|
||||||
|
2789,
|
||||||
|
4312
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "real_estate_docs",
|
"key": "real_estate_docs",
|
||||||
"label": "Справка о недвижимости",
|
"label": "Справка о недвижимости",
|
||||||
"prev": 0,
|
|
||||||
"cur": 78,
|
"cur": 78,
|
||||||
|
"prev": 0,
|
||||||
"growth": null,
|
"growth": null,
|
||||||
"is_new": true
|
"is_new": true,
|
||||||
|
"cur_cum": [
|
||||||
|
0,
|
||||||
|
23,
|
||||||
|
44,
|
||||||
|
52,
|
||||||
|
70,
|
||||||
|
78
|
||||||
|
],
|
||||||
|
"prev_cum": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@ -477,34 +948,47 @@ function renderCards() {
|
|||||||
const el = document.getElementById('cards');
|
const el = document.getElementById('cards');
|
||||||
if (!list.length) { el.innerHTML = '<div class="empty-note">Ничего не найдено</div>'; return; }
|
if (!list.length) { el.innerHTML = '<div class="empty-note">Ничего не найдено</div>'; return; }
|
||||||
|
|
||||||
|
const labels = d.month_labels || [];
|
||||||
el.innerHTML = list.map(m => {
|
el.innerHTML = list.map(m => {
|
||||||
const cls = classify(m);
|
const cls = classify(m);
|
||||||
const max = Math.max(m.cur, m.prev, 1);
|
const curCum = m.cur_cum || [m.cur];
|
||||||
const curW = (m.cur / max * 100).toFixed(1);
|
const prevCum = m.prev_cum || [m.prev];
|
||||||
const prevW = (m.prev / max * 100).toFixed(1);
|
|
||||||
return `
|
return `
|
||||||
<div class="card ${cls}">
|
<div class="card ${cls}">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<div class="card-title">${m.label}</div>
|
<div>
|
||||||
|
<div class="card-title">${m.label}</div>
|
||||||
|
<div class="card-sub">нарастающим итогом · ${d.cur_year} vs ${d.prev_year}</div>
|
||||||
|
</div>
|
||||||
<span class="badge ${cls}">${badgeText(m, cls)}</span>
|
<span class="badge ${cls}">${badgeText(m, cls)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-value">${fmtInt(m.cur)}<span class="card-value-year"> · ${d.cur_year}</span></div>
|
<div class="card-value">${fmtInt(m.cur)}<span class="card-value-year"> · ${d.cur_year}</span></div>
|
||||||
<div class="cmp">
|
${sparkline(curCum, prevCum)}
|
||||||
<div class="cmp-row">
|
<div class="spark-x"><span>${labels[0] || ''}</span><span>${labels[labels.length - 1] || ''}</span></div>
|
||||||
<span class="cmp-year">${d.cur_year}</span>
|
<div class="legend">
|
||||||
<span class="cmp-track"><span class="cmp-fill cur" style="width:${curW}%"></span></span>
|
<span><i class="dot cur"></i>${d.cur_year}: <b>${fmtInt(m.cur)}</b></span>
|
||||||
<span class="cmp-num">${fmtInt(m.cur)}</span>
|
<span><i class="dot prev"></i>${d.prev_year}: <b>${fmtInt(m.prev)}</b></span>
|
||||||
</div>
|
|
||||||
<div class="cmp-row">
|
|
||||||
<span class="cmp-year">${d.prev_year}</span>
|
|
||||||
<span class="cmp-track"><span class="cmp-fill prev" style="width:${prevW}%"></span></span>
|
|
||||||
<span class="cmp-num">${fmtInt(m.prev)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Накопительный мини-график: две кривые (пунктир — прошлый год, сплошная — текущий)
|
||||||
|
function sparkline(curCum, prevCum) {
|
||||||
|
const W = 100, H = 34, pad = 3;
|
||||||
|
const n = Math.max(curCum.length, prevCum.length, 1);
|
||||||
|
const max = Math.max(1, ...curCum, ...prevCum);
|
||||||
|
const x = i => n <= 1 ? W / 2 : pad + i * (W - 2 * pad) / (n - 1);
|
||||||
|
const y = v => H - pad - (v / max) * (H - 2 * pad);
|
||||||
|
const pts = arr => arr.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(' ');
|
||||||
|
const lastI = curCum.length - 1;
|
||||||
|
return `<svg class="spark" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">
|
||||||
|
<polyline class="sp-prev" points="${pts(prevCum)}"/>
|
||||||
|
<polyline class="sp-cur" points="${pts(curCum)}"/>
|
||||||
|
<circle class="dot-cur" cx="${x(lastI).toFixed(1)}" cy="${y(curCum[lastI] || 0).toFixed(1)}" r="2.2"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderAll() {
|
function renderAll() {
|
||||||
document.getElementById('status-msg').style.display = 'none';
|
document.getElementById('status-msg').style.display = 'none';
|
||||||
document.getElementById('content').style.display = 'block';
|
document.getElementById('content').style.display = 'block';
|
||||||
|
|||||||
@ -3,11 +3,17 @@
|
|||||||
"""
|
"""
|
||||||
Ежедневное обновление статистики «Метрики МП» из Impala.
|
Ежедневное обновление статистики «Метрики МП» из Impala.
|
||||||
|
|
||||||
Сравнивает использование функций мобильного приложения за текущий год
|
Модель данных:
|
||||||
(с 1 января по ВЧЕРАШНИЙ день) с аналогичным периодом прошлого года.
|
• Грузим помесячно (группировка по report_period_id = YYYYmm), начиная с января
|
||||||
Результат пишется в ../app_stats/app_metrics.json и пушится в ветку pages —
|
прошлого года и до текущего месяца текущего года.
|
||||||
его читает страница app_stats/index.html.
|
• Текущий (незавершённый) месяц обрезаем по entry_date до ВЧЕРАШНЕГО дня —
|
||||||
|
симметрично для обоих лет (например, на 16 июня берём данные по 15 июня
|
||||||
|
включительно и в 2026, и в 2025), чтобы сравнение было «равный период».
|
||||||
|
• Внутри каждого года считаем нарастающий итог (кумулятив) по месяцам; на старте
|
||||||
|
нового года накопление сбрасывается.
|
||||||
|
• Сравнение в карточке = кумулятив-на-дату текущего года vs прошлого года.
|
||||||
|
|
||||||
|
Результат пишется в ../app_stats/app_metrics.json и пушится в ветку pages.
|
||||||
Подключение к Impala, конфиг и git-push переиспользуются из update_kpi.py.
|
Подключение к Impala, конфиг и git-push переиспользуются из update_kpi.py.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -54,31 +60,43 @@ METRICS = [
|
|||||||
("turbo_button", "Turbo кнопка"),
|
("turbo_button", "Turbo кнопка"),
|
||||||
("real_estate_docs", "Справка о недвижимости"),
|
("real_estate_docs", "Справка о недвижимости"),
|
||||||
]
|
]
|
||||||
|
METRIC_KEYS = [k for k, _ in METRICS]
|
||||||
|
|
||||||
_MONTHS_RU = ["", "января", "февраля", "марта", "апреля", "мая", "июня",
|
_MONTHS_RU_GEN = ["", "января", "февраля", "марта", "апреля", "мая", "июня",
|
||||||
"июля", "августа", "сентября", "октября", "ноября", "декабря"]
|
"июля", "августа", "сентября", "октября", "ноября", "декабря"]
|
||||||
|
_MONTHS_RU_SHORT = ["", "Янв", "Фев", "Мар", "Апр", "Май", "Июн",
|
||||||
|
"Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"]
|
||||||
|
|
||||||
|
|
||||||
def date_range(today: dt.date | None = None):
|
def date_bounds(today: dt.date | None = None):
|
||||||
"""Возвращает (cur_year, prev_year, start_cur, end_cur, start_prev, end_prev) как date."""
|
"""Границы выборки. Текущий месяц обрезается по вчерашнему дню (симметрично по годам)."""
|
||||||
today = today or dt.date.today()
|
today = today or dt.date.today()
|
||||||
end_cur = today - dt.timedelta(days=1) # вчера
|
end_cur = today - dt.timedelta(days=1) # вчера
|
||||||
cur_year = today.year
|
cur_year = today.year
|
||||||
prev_year = cur_year - 1
|
prev_year = cur_year - 1
|
||||||
|
cap_month, cap_day = end_cur.month, end_cur.day
|
||||||
|
|
||||||
start_cur = dt.date(cur_year, 1, 1)
|
start_cur = dt.date(cur_year, 1, 1)
|
||||||
start_prev = dt.date(prev_year, 1, 1)
|
start_prev = dt.date(prev_year, 1, 1)
|
||||||
# та же дата прошлого года; подстраховка на 29 февраля
|
|
||||||
try:
|
try:
|
||||||
end_prev = dt.date(prev_year, end_cur.month, end_cur.day)
|
end_prev = dt.date(prev_year, cap_month, cap_day)
|
||||||
except ValueError:
|
except ValueError: # 29 февраля в невисокосный год
|
||||||
end_prev = dt.date(prev_year, end_cur.month, 28)
|
end_prev = dt.date(prev_year, cap_month, 28)
|
||||||
return cur_year, prev_year, start_cur, end_cur, start_prev, end_prev
|
return {
|
||||||
|
"cur_year": cur_year, "prev_year": prev_year,
|
||||||
|
"cap_month": cap_month, "cap_day": cap_day,
|
||||||
|
"start_cur": start_cur, "end_cur": end_cur,
|
||||||
|
"start_prev": start_prev, "end_prev": end_prev,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_sql(start_cur, end_cur, start_prev, end_prev) -> str:
|
def build_sql(b: dict) -> str:
|
||||||
|
# created_at — timestamp, поэтому верхнюю границу берём как «< следующий день»
|
||||||
|
cur_end_excl = b["end_cur"] + dt.timedelta(days=1)
|
||||||
|
prev_end_excl = b["end_prev"] + dt.timedelta(days=1)
|
||||||
return f"""
|
return f"""
|
||||||
with t as (
|
with t as (
|
||||||
select round(report_period_id/100) as report_year,
|
select report_period_id,
|
||||||
count(case when event_type = 'OPENWSCREENMYSERVICES' then 1 end) as my_services,
|
count(case when event_type = 'OPENWSCREENMYSERVICES' then 1 end) as my_services,
|
||||||
count(case when event_type = 'OPENWINDOWDETALIZTION' then 1 end) as traffic,
|
count(case when event_type = 'OPENWINDOWDETALIZTION' then 1 end) as traffic,
|
||||||
count(case when event_type = 'OPENWINDOWPAYMENT' then 1 end) as payments,
|
count(case when event_type = 'OPENWINDOWPAYMENT' then 1 end) as payments,
|
||||||
@ -107,77 +125,104 @@ with t as (
|
|||||||
count(case when event_type = 'turbo_activation_success_viewed' then 1 end) as turbo_button,
|
count(case when event_type = 'turbo_activation_success_viewed' then 1 end) as turbo_button,
|
||||||
count(case when event_type = 'real_estate_docs_screen_shown' then 1 end) as real_estate_docs
|
count(case when event_type = 'real_estate_docs_screen_shown' then 1 end) as real_estate_docs
|
||||||
from drb.drb_iliyas_amplitude_metrics_full
|
from drb.drb_iliyas_amplitude_metrics_full
|
||||||
where entry_date between '{start_cur:%Y-%m-%d}' and '{end_cur:%Y-%m-%d}'
|
where entry_date between '{b['start_cur']:%Y-%m-%d}' and '{b['end_cur']:%Y-%m-%d}'
|
||||||
or entry_date between '{start_prev:%Y-%m-%d}' and '{end_prev:%Y-%m-%d}'
|
or entry_date between '{b['start_prev']:%Y-%m-%d}' and '{b['end_prev']:%Y-%m-%d}'
|
||||||
group by 1
|
group by report_period_id
|
||||||
)
|
)
|
||||||
, a as (
|
, a as (
|
||||||
select year(created_at) as report_year, count(order_id) as adsl
|
select (year(created_at) * 100 + month(created_at)) as report_period_id,
|
||||||
|
count(order_id) as adsl
|
||||||
from telecomkz.telecomkz_retention_service_prod_tariff_change_validations
|
from telecomkz.telecomkz_retention_service_prod_tariff_change_validations
|
||||||
|
where (created_at >= '{b['start_cur']:%Y-%m-%d}' and created_at < '{cur_end_excl:%Y-%m-%d}')
|
||||||
|
or (created_at >= '{b['start_prev']:%Y-%m-%d}' and created_at < '{prev_end_excl:%Y-%m-%d}')
|
||||||
group by 1
|
group by 1
|
||||||
)
|
)
|
||||||
select t.report_year, my_services, traffic, payments, orders, loyalty, pay, billing_detail, viktorina, partners, tv_plus, boosters, roaming, pereoform, aitu_music, online_booking, my_docs, dz_statement, new_boosters_roaming_kcell, adsl, law_and_order, acs, kaspi_freedom_pay, csat, multicustomer,
|
select t.report_period_id, my_services, traffic, payments, orders, loyalty, pay, billing_detail, viktorina, partners, tv_plus, boosters, roaming, pereoform, aitu_music, online_booking, my_docs, dz_statement, new_boosters_roaming_kcell, adsl, law_and_order, acs, kaspi_freedom_pay, csat, multicustomer,
|
||||||
tv_plus_setup, static_ip, turbo_button, real_estate_docs
|
tv_plus_setup, static_ip, turbo_button, real_estate_docs
|
||||||
from t
|
from t
|
||||||
left join a on t.report_year = a.report_year
|
left join a on t.report_period_id = a.report_period_id
|
||||||
order by t.report_year
|
order by t.report_period_id
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
def fetch_by_year(conn, sql):
|
def fetch_monthly(conn, sql):
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
log.info("Выполнение запроса метрик МП...")
|
log.info("Выполнение запроса метрик МП (помесячно)...")
|
||||||
cur.execute(sql)
|
cur.execute(sql)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
names = [d[0].lower() for d in cur.description]
|
names = [d[0].lower() for d in cur.description]
|
||||||
cur.close()
|
cur.close()
|
||||||
idx = {n: i for i, n in enumerate(names)}
|
idx = {n: i for i, n in enumerate(names)}
|
||||||
by_year = {}
|
out = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
year = int(round(float(r[idx["report_year"]])))
|
rec = {"report_period_id": int(r[idx["report_period_id"]])}
|
||||||
by_year[year] = {name: r[idx[name]] for name in idx}
|
for key in METRIC_KEYS:
|
||||||
log.info("Получено годовых строк: %s", sorted(by_year))
|
v = r[idx[key]] if key in idx else None
|
||||||
return by_year, idx
|
rec[key] = int(v) if v is not None else 0
|
||||||
|
out.append(rec)
|
||||||
|
log.info("Получено помесячных строк: %d (периоды: %s)",
|
||||||
|
len(out), ", ".join(str(x["report_period_id"]) for x in out))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _num(v):
|
def _cumsum(arr):
|
||||||
return int(v) if v is not None else 0
|
acc, out = 0, []
|
||||||
|
for v in arr:
|
||||||
|
acc += v
|
||||||
|
out.append(acc)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def build_payload(by_year, cur_year, prev_year, start_cur, end_cur):
|
def build_payload(rows, b: dict):
|
||||||
cur_row = by_year.get(cur_year, {})
|
cur_year, prev_year, cap_month = b["cur_year"], b["prev_year"], b["cap_month"]
|
||||||
prev_row = by_year.get(prev_year, {})
|
months = list(range(1, cap_month + 1))
|
||||||
|
month_labels = [_MONTHS_RU_SHORT[m] for m in months]
|
||||||
|
|
||||||
|
# key -> {year -> [monthly values по месяцам months]}
|
||||||
|
monthly = {k: {cur_year: [0] * len(months), prev_year: [0] * len(months)} for k in METRIC_KEYS}
|
||||||
|
for rec in rows:
|
||||||
|
rpid = rec["report_period_id"]
|
||||||
|
y, m = rpid // 100, rpid % 100
|
||||||
|
if y not in (cur_year, prev_year) or m not in months:
|
||||||
|
continue
|
||||||
|
i = months.index(m)
|
||||||
|
for k in METRIC_KEYS:
|
||||||
|
monthly[k][y][i] = rec[k]
|
||||||
|
|
||||||
metrics = []
|
metrics = []
|
||||||
for key, label in METRICS:
|
for key, label in METRICS:
|
||||||
cur_v = _num(cur_row.get(key))
|
cur_cum = _cumsum(monthly[key][cur_year])
|
||||||
prev_v = _num(prev_row.get(key))
|
prev_cum = _cumsum(monthly[key][prev_year])
|
||||||
|
cur_v = cur_cum[-1] if cur_cum else 0
|
||||||
|
prev_v = prev_cum[-1] if prev_cum else 0
|
||||||
is_new = prev_v == 0
|
is_new = prev_v == 0
|
||||||
growth = None if is_new else (cur_v - prev_v) / prev_v
|
growth = None if is_new else (cur_v - prev_v) / prev_v
|
||||||
metrics.append({
|
metrics.append({
|
||||||
"key": key, "label": label,
|
"key": key, "label": label,
|
||||||
"prev": prev_v, "cur": cur_v,
|
"cur": cur_v, "prev": prev_v,
|
||||||
"growth": growth, "is_new": is_new,
|
"growth": growth, "is_new": is_new,
|
||||||
|
"cur_cum": cur_cum, "prev_cum": prev_cum,
|
||||||
})
|
})
|
||||||
|
|
||||||
period_label = (f"с 1 января по {end_cur.day} {_MONTHS_RU[end_cur.month]}")
|
end_cur = b["end_cur"]
|
||||||
|
period_label = f"с 1 января по {end_cur.day} {_MONTHS_RU_GEN[end_cur.month]}"
|
||||||
return {
|
return {
|
||||||
"generated_at": dt.datetime.now().isoformat(timespec="seconds"),
|
"generated_at": dt.datetime.now().isoformat(timespec="seconds"),
|
||||||
"cur_year": cur_year,
|
"cur_year": cur_year,
|
||||||
"prev_year": prev_year,
|
"prev_year": prev_year,
|
||||||
"period_label": period_label,
|
"period_label": period_label,
|
||||||
"range": {"start": f"{start_cur:%Y-%m-%d}", "end": f"{end_cur:%Y-%m-%d}"},
|
"range": {"start": f"{b['start_cur']:%Y-%m-%d}", "end": f"{end_cur:%Y-%m-%d}"},
|
||||||
|
"months": months,
|
||||||
|
"month_labels": month_labels,
|
||||||
|
"cumulative": True,
|
||||||
"metrics": metrics,
|
"metrics": metrics,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def write_if_changed(payload) -> bool:
|
def write_if_changed(payload) -> bool:
|
||||||
text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
||||||
old = ""
|
old = OUT_PATH.read_text(encoding="utf-8") if OUT_PATH.exists() else ""
|
||||||
if OUT_PATH.exists():
|
|
||||||
old = OUT_PATH.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
# Сравниваем без учёта generated_at (чтобы не коммитить, если данные те же)
|
|
||||||
def strip_ts(s):
|
def strip_ts(s):
|
||||||
try:
|
try:
|
||||||
d = json.loads(s)
|
d = json.loads(s)
|
||||||
@ -192,7 +237,8 @@ def write_if_changed(payload) -> bool:
|
|||||||
|
|
||||||
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
OUT_PATH.write_text(text, encoding="utf-8")
|
OUT_PATH.write_text(text, encoding="utf-8")
|
||||||
log.info("app_metrics.json обновлён (%d метрик).", len(payload["metrics"]))
|
log.info("app_metrics.json обновлён (%d метрик, %d мес.).",
|
||||||
|
len(payload["metrics"]), len(payload["months"]))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -201,26 +247,27 @@ def main() -> int:
|
|||||||
log.info("Старт обновления Метрик МП")
|
log.info("Старт обновления Метрик МП")
|
||||||
cfg = base.load_config()
|
cfg = base.load_config()
|
||||||
|
|
||||||
cur_year, prev_year, start_cur, end_cur, start_prev, end_prev = date_range()
|
b = date_bounds()
|
||||||
log.info("Период: %s..%s (тек.) и %s..%s (пред.)",
|
log.info("Период (тек.): %s..%s | (пред.): %s..%s | месяцев: %d",
|
||||||
start_cur, end_cur, start_prev, end_prev)
|
b["start_cur"], b["end_cur"], b["start_prev"], b["end_prev"], b["cap_month"])
|
||||||
sql = build_sql(start_cur, end_cur, start_prev, end_prev)
|
sql = build_sql(b)
|
||||||
|
|
||||||
base.patch_thrift_ssl()
|
base.patch_thrift_ssl()
|
||||||
conn = base.connect_impala(cfg)
|
conn = base.connect_impala(cfg)
|
||||||
try:
|
try:
|
||||||
by_year, _ = fetch_by_year(conn, sql)
|
rows = fetch_monthly(conn, sql)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if cur_year not in by_year:
|
cur_year = b["cur_year"]
|
||||||
|
if not any(r["report_period_id"] // 100 == cur_year for r in rows):
|
||||||
log.error("В ответе нет данных за %s — JSON НЕ перезаписан.", cur_year)
|
log.error("В ответе нет данных за %s — JSON НЕ перезаписан.", cur_year)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
payload = build_payload(by_year, cur_year, prev_year, start_cur, end_cur)
|
payload = build_payload(rows, b)
|
||||||
if write_if_changed(payload):
|
if write_if_changed(payload):
|
||||||
base.git_commit_push(cfg, [OUT_REL],
|
base.git_commit_push(cfg, [OUT_REL],
|
||||||
f"data: update app metrics {dt.date.today():%Y-%m-%d}")
|
f"data: update app metrics {dt.date.today():%Y-%m-%d}")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user