From 7fc7ec61b948fb91f603902007e98744626d0369 Mon Sep 17 00:00:00 2001
From: phoenix <vova24848@gmail.com>
Date: Wed, 23 Oct 2024 13:30:27 +0300
Subject: [PATCH 1/2] Add chart for health check

---
 CHANGELOG.md                                  |   3 +
 client/package-lock.json                      |  18 ++-
 client/package.json                           |   3 +-
 client/src/main.ts                            |   2 +
 client/src/plugins/chartjs.ts                 |  20 +++
 .../src/views/HealthCheck/HealthCheckView.vue |  20 +--
 .../views/HealthCheck/HealthDay.service.ts    |  83 ++++++++++++
 .../views/HealthCheck/HealthTargetView.vue    | 126 +++++++++++-------
 .../src/views/Project/Status/StatusRect.vue   |   3 +-
 client/src/views/Utils.ts                     |   3 +
 .../api/job/health_request_job.py             |  20 +--
 11 files changed, 227 insertions(+), 74 deletions(-)
 create mode 100644 client/src/plugins/chartjs.ts
 create mode 100644 client/src/views/HealthCheck/HealthDay.service.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index afd1025..c2f7313 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - [Add widget for moderate users permissions](https://github.com/PhoenixNazarov/prompt-admin/pull/25)
 - [Create HealthCheck status system](https://github.com/PhoenixNazarov/prompt-admin/pull/27)
 - [Upgrade status time react, add description](https://github.com/PhoenixNazarov/prompt-admin/pull/27)
+- Update design: Add borders, add resizable
+- Add Charts for healthCheck
 
 ### Changed
 
@@ -47,3 +49,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fix start loading a list table component without a filter
 - Bugfix load popup tables, back home, ident for test_case
 - [Fix /auth/me return user password, now return password: null](https://github.com/PhoenixNazarov/prompt-admin/pull/25) 
+- Fix health check, add timezone
\ No newline at end of file
diff --git a/client/package-lock.json b/client/package-lock.json
index 2e04d45..5abaadd 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -18,7 +18,8 @@
         "ajv-errors": "^3.0.0",
         "ajv-formats": "^3.0.1",
         "axios": "==1.7.4",
-        "chart.js": "^4.4.4",
+        "chart.js": "^4.4.5",
+        "chartjs-adapter-moment": "^1.0.1",
         "dedent": "^1.5.3",
         "json-editor-vue": "^0.15.1",
         "moment": "^2.30.1",
@@ -1310,9 +1311,9 @@
       }
     },
     "node_modules/chart.js": {
-      "version": "4.4.4",
-      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz",
-      "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==",
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz",
+      "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==",
       "dependencies": {
         "@kurkle/color": "^0.3.0"
       },
@@ -1320,6 +1321,15 @@
         "pnpm": ">=8"
       }
     },
+    "node_modules/chartjs-adapter-moment": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz",
+      "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==",
+      "peerDependencies": {
+        "chart.js": ">=3.0.0",
+        "moment": "^2.10.2"
+      }
+    },
     "node_modules/chokidar": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
diff --git a/client/package.json b/client/package.json
index d04d524..2e11b18 100644
--- a/client/package.json
+++ b/client/package.json
@@ -20,7 +20,8 @@
     "ajv-errors": "^3.0.0",
     "ajv-formats": "^3.0.1",
     "axios": "==1.7.4",
-    "chart.js": "^4.4.4",
+    "chart.js": "^4.4.5",
+    "chartjs-adapter-moment": "^1.0.1",
     "dedent": "^1.5.3",
     "json-editor-vue": "^0.15.1",
     "moment": "^2.30.1",
diff --git a/client/src/main.ts b/client/src/main.ts
index b6b3150..70ad5b1 100644
--- a/client/src/main.ts
+++ b/client/src/main.ts
@@ -7,6 +7,7 @@ import {installFortAwesome} from "./plugins/fortawesome.ts";
 import {installVuetify} from "./plugins/vuetify.ts";
 import {installRouter} from "./plugins/router.ts";
 import {installPrimevue} from "./plugins/primevue.ts";
+import {installChartJs} from "./plugins/chartjs.ts";
 
 
 const app = createApp(App)
@@ -18,6 +19,7 @@ installFortAwesome(app)
 installVuetify(app)
 installRouter(app)
 installPrimevue(app)
+installChartJs(app)
 
 
 // Mount
diff --git a/client/src/plugins/chartjs.ts b/client/src/plugins/chartjs.ts
new file mode 100644
index 0000000..af12b39
--- /dev/null
+++ b/client/src/plugins/chartjs.ts
@@ -0,0 +1,20 @@
+import {
+    BarElement,
+    CategoryScale,
+    Chart as ChartJS,
+    Legend,
+    LinearScale,
+    LineElement,
+    LogarithmicScale,
+    PointElement,
+    TimeScale,
+    Title,
+    Tooltip
+} from 'chart.js'
+import type {App} from "vue";
+import 'chartjs-adapter-moment';
+
+export function installChartJs(app: App) {
+    ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, LogarithmicScale, LineElement, PointElement)
+}
+
diff --git a/client/src/views/HealthCheck/HealthCheckView.vue b/client/src/views/HealthCheck/HealthCheckView.vue
index 09e9792..bfdae88 100644
--- a/client/src/views/HealthCheck/HealthCheckView.vue
+++ b/client/src/views/HealthCheck/HealthCheckView.vue
@@ -19,16 +19,18 @@ export default defineComponent({
 </script>
 
 <template>
-  <VContainer>
-    <VCard>
-      <VCardTitle>Health Monitor</VCardTitle>
-      <VCardText>
-        <VSkeletonLoader type="card" v-if="healthCheckStore.loadings.targets"/>
+  <div class="outer-y" style="height: calc(100vh - 3rem)">
+    <VContainer>
+      <VCard>
+        <VCardTitle>Health Monitor</VCardTitle>
+        <VCardText>
+          <VSkeletonLoader type="card" v-if="healthCheckStore.loadings.targets"/>
 
-        <HealthTargetView v-else :health-target="target" v-for="target in healthCheckStore.targets"/>
-      </VCardText>
-    </VCard>
-  </VContainer>
+          <HealthTargetView v-else :health-target="target" v-for="target in healthCheckStore.targets"/>
+        </VCardText>
+      </VCard>
+    </VContainer>
+  </div>
 </template>
 
 <style scoped>
diff --git a/client/src/views/HealthCheck/HealthDay.service.ts b/client/src/views/HealthCheck/HealthDay.service.ts
new file mode 100644
index 0000000..d852a48
--- /dev/null
+++ b/client/src/views/HealthCheck/HealthDay.service.ts
@@ -0,0 +1,83 @@
+import {HealthDay, HealthTarget, useHealthCheckStore} from "../../stores/healthcheck.store.ts";
+import {equalDate} from "../Utils.ts";
+
+export class HealthDayService {
+    private healthDay: HealthDay | undefined
+
+    constructor(healthDay: HealthDay | undefined) {
+        this.healthDay = healthDay
+    }
+
+    static fromDay(healthTarget: HealthTarget, d: number) {
+        const healthCheckStore = useHealthCheckStore()
+        const date = this.showDate(d)
+        const healthDay = healthCheckStore.getDays(healthTarget)?.find(el => {
+            const day = new Date(el.date)
+            return equalDate(date, day)
+        })
+        return new HealthDayService(healthDay)
+    }
+
+    static showDate(d: number) {
+        const date = new Date();
+        date.setDate(date.getDate() - d + 1);
+        return date
+    }
+
+    getStatus() {
+        const fallTimes = this.getFallTimes()
+        if (fallTimes == undefined) return 'Lost'
+        if (fallTimes > 0) {
+            return 'Downtime'
+        }
+        return `Operational`
+    }
+
+    getDescription() {
+        const fallTimes = this.getFallTimes()
+        const lostTimes = this.getLostTimes()
+        let res = ''
+        if (fallTimes != undefined && fallTimes > 0) {
+            res += `Down for ${fallTimes} minutes`
+        }
+        if (lostTimes != undefined && lostTimes > 0) {
+            res += `<br>Lost for ${lostTimes} minutes`
+        }
+        return res;
+    }
+
+    getPercentage() {
+        const fallTimes = this.getFallTimes()
+        const lostTimes = this.getLostTimes()
+        if (!fallTimes) return undefined
+        return (fallTimes / (60 * 24)) * 100 + (lostTimes / (60 * 24)) * 30
+    }
+
+    getFallTimes() {
+        const lostTimes = this.getLostTimes()
+        if (!this.healthDay || lostTimes == undefined) return
+        return this.healthDay.fall_times
+    }
+
+    getLostTimes() {
+        const maxTimes = this.getMaxTimes()
+        const responseTime = this.healthDay?.count_response_time
+        if (responseTime == undefined) return 0
+        const lostTimes = maxTimes - responseTime
+        return lostTimes > 30 ? lostTimes : 0
+    }
+
+    getMaxTimes() {
+        let maxTimes = 60 * 24
+        if (this.isToday()) {
+            const now = new Date()
+            return now.getHours() * 24 + now.getMinutes()
+        }
+        return maxTimes
+    }
+
+    isToday() {
+        if (!this.healthDay) return
+        return equalDate(new Date(), new Date(this.healthDay.date))
+    }
+}
\ No newline at end of file
diff --git a/client/src/views/HealthCheck/HealthTargetView.vue b/client/src/views/HealthCheck/HealthTargetView.vue
index 4a79ad5..a5a5da0 100644
--- a/client/src/views/HealthCheck/HealthTargetView.vue
+++ b/client/src/views/HealthCheck/HealthTargetView.vue
@@ -1,12 +1,15 @@
 <script lang="ts">
 import {defineComponent, PropType} from 'vue'
-import {HealthDay, HealthTarget, useHealthCheckStore} from "../../stores/healthcheck.store.ts";
+import {HealthTarget, useHealthCheckStore} from "../../stores/healthcheck.store.ts";
 import {FontAwesomeIcon} from "@fortawesome/vue-fontawesome";
 import StatusRect from "../Project/Status/StatusRect.vue";
+import {HealthDayService} from "./HealthDay.service.ts";
+import {Line} from 'vue-chartjs'
+import moment from "moment";
 
 export default defineComponent({
   name: "HealthTargetView",
-  components: {StatusRect, FontAwesomeIcon},
+  components: {StatusRect, FontAwesomeIcon, Line},
   props: {
     healthTarget: {
       type: Object as PropType<HealthTarget>,
@@ -19,6 +22,44 @@ export default defineComponent({
       healthCheckStore
     }
   },
+  computed: {
+    options() {
+      return {
+        plugins: {
+          tooltip: {
+            mode: 'index',
+            intersect: false
+          },
+          legend: {
+            display: false // это отключает отображение легенды на графике
+          },
+        },
+        elements: {
+          point: {
+            radius: 0.1
+          }
+        },
+        scales: {
+          y: {
+            ticks: {
+              // Include a dollar sign in the ticks
+              callback: function(value, index, ticks) {
+                return value + ' s';
+              }
+            },
+          },
+          x: {
+            type: 'time',
+            grid: {
+              display: false
+            }
+          }
+        },
+        responsive: true,
+        maintainAspectRatio: false,
+      }
+    }
+  },
   methods: {
     getTargetStatus() {
       const lastUnit = this.healthCheckStore.getLastUnit(this.healthTarget)
@@ -27,52 +68,33 @@ export default defineComponent({
       if (timeSpent > 180)
         return;
 
-      return lastUnit.status;
-    },
-    getStatus(d: number) {
-      const day = this.getDay(d)
-      if (day == undefined) return 'Undefined'
-      const fallTimes = this.getFallTimes(day)
-      if (fallTimes > 0) {
-        return 'Downtime'
-      }
-      return `Operational`
+      return lastUnit.status
     },
-    getDescription(d: number) {
-      const day = this.getDay(d)
-      if (day == undefined) return undefined
-      const fallTimes = this.getFallTimes(day)
-      if (fallTimes > 0) {
-        return `Down for ${fallTimes} minutes`
+    getChartData() {
+      const units = this.healthCheckStore.getUnits(this.healthTarget)?.reverse()
+      if (units == undefined) return
+      return {
+        labels: units?.map(el => moment(el.request_datetime)),
+        datasets: [{
+          label: 'Response Time',
+          data: units?.map(el => el.response_time),
+          borderWidth: 1,
+          borderColor: 'rgb(0,83,255)',
+        }]
       }
     },
-    getPercentage(d: number) {
-      const day = this.getDay(d)
-      if (day == undefined) return undefined
-      const fallTimes = this.getFallTimes(day)
-      return (fallTimes / (60 * 24)) * 200
-    },
-    getFallTimes(day: HealthDay) {
-      let maxTimes = 60 * 24
-      if (day.id == this.getDay(1)?.id) {
-        const now = new Date()
-        maxTimes = now.getHours() * 24 + now.getMinutes()
+    buildDayStatuses() {
+      const result = []
+      for (let i = 1; i < 90; i++) {
+        const healthDayService = HealthDayService.fromDay(this.healthTarget, i);
+        result.push({
+          percentage: healthDayService.getPercentage(),
+          date: HealthDayService.showDate(i),
+          status: healthDayService.getStatus(),
+          description: healthDayService.getDescription(),
+        })
       }
-
-      const lostTimes = maxTimes - day.count_response_time
-      return day.fall_times + lostTimes > 5 ? lostTimes : 0
-    },
-    showDate(d: number) {
-      const date = new Date();
-      date.setDate(date.getDate() - d + 1);
-      return date
-    },
-    getDay(d: number) {
-      const date = this.showDate(d)
-      return this.healthCheckStore.getDays(this.healthTarget)?.find(el => {
-        const day = new Date(el.date)
-        return day.getDate() == date.getDate() && day.getMonth() == date.getMonth() && day.getFullYear() == date.getFullYear()
-      })
+      return result
     }
   }
 })
@@ -98,13 +120,19 @@ export default defineComponent({
     <VCardText>
       <div class="chart">
         <StatusRect
-            :percentage="getPercentage(d)"
-            :date="showDate(d)"
-            :status="getStatus(d)"
-            :description="getDescription(d)"
-            v-for="d in 90"
+            :percentage="d.percentage"
+            :date="d.date"
+            :status="d.status"
+            :description="d.description"
+            v-for="d in buildDayStatuses()"
         />
       </div>
+      <p class="ma-1">
+        Response Times:
+      </p>
+      <div style="height: 10rem" v-if="getChartData()">
+        <Line :data="getChartData()" :options="options"/>
+      </div>
     </VCardText>
   </VCard>
 </template>
diff --git a/client/src/views/Project/Status/StatusRect.vue b/client/src/views/Project/Status/StatusRect.vue
index 896d9e6..01dd9ed 100644
--- a/client/src/views/Project/Status/StatusRect.vue
+++ b/client/src/views/Project/Status/StatusRect.vue
@@ -50,8 +50,7 @@ export default defineComponent({
       <div v-if="status">
         {{ status }}
       </div>
-      <div v-if="description">
-        {{ description }}
+      <div v-if="description" v-html="description">
       </div>
       <div>
         {{ dateFormat(date) }}
diff --git a/client/src/views/Utils.ts b/client/src/views/Utils.ts
index bd68cac..8ced0c3 100644
--- a/client/src/views/Utils.ts
+++ b/client/src/views/Utils.ts
@@ -55,3 +55,6 @@ export function chunk<T>(arr: T[], size: number): T[][] {
     );
 }
 
+export function equalDate(a: Date, b: Date) {
+    return a.getDate() == b.getDate() && a.getMonth() == b.getMonth() && a.getFullYear() == b.getFullYear()
+}
\ No newline at end of file
diff --git a/server/promptadmin_server/api/job/health_request_job.py b/server/promptadmin_server/api/job/health_request_job.py
index 4276ab2..a78e9d5 100644
--- a/server/promptadmin_server/api/job/health_request_job.py
+++ b/server/promptadmin_server/api/job/health_request_job.py
@@ -45,15 +45,17 @@ async def polling(self):
 
     async def status_target(self, target: HealthTarget):
         t_start = time.time()
-
-        try:
-            async with httpx.AsyncClient() as client:
-                r = await client.get(target.url)
-            status = r.is_success
-            if status is None:
-                print(r.status_code)
-        except Exception as e:
-            status = False
+        status = False
+        for i in range(3):
+            t_start = time.time()
+            try:
+                async with httpx.AsyncClient() as client:
+                    r = await client.get(target.url)
+                status = r.is_success
+                break
+            except Exception as e:
+                status = False
+                continue
 
         health_unit = HealthUnit(
             request_datetime=datetime.datetime.now(pytz.utc),

From a051758db2eda329fb355595d39ec2fd68921603 Mon Sep 17 00:00:00 2001
From: phoenix <vova24848@gmail.com>
Date: Wed, 23 Oct 2024 13:33:54 +0300
Subject: [PATCH 2/2] bugfix display zero permission value

---
 client/src/views/HealthCheck/HealthDay.service.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/client/src/views/HealthCheck/HealthDay.service.ts b/client/src/views/HealthCheck/HealthDay.service.ts
index d852a48..f34fb83 100644
--- a/client/src/views/HealthCheck/HealthDay.service.ts
+++ b/client/src/views/HealthCheck/HealthDay.service.ts
@@ -47,15 +47,15 @@ export class HealthDayService {
     }
 
     getPercentage() {
+        if (this.healthDay == undefined) return
         const fallTimes = this.getFallTimes()
         const lostTimes = this.getLostTimes()
         if (!fallTimes) return undefined
-        return (fallTimes / (60 * 24)) * 100 + (lostTimes / (60 * 24)) * 30
+        return (fallTimes / this.healthDay.count_response_time) * 100 + (lostTimes / (60 * 24)) * 30
     }
 
     getFallTimes() {
-        const lostTimes = this.getLostTimes()
-        if (!this.healthDay || lostTimes == undefined) return
+        if (!this.healthDay) return
         return this.healthDay.fall_times
     }