- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 6.9k
 
feat(status page): Add configurable heartbeat bar range options (100beats, 1..365 days) #5916
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
          
     Draft
      
      
            peaktwilight
  wants to merge
  69
  commits into
  louislam:master
  
    
      
        
          
  
    
      Choose a base branch
      
     
    
      
        
      
      
        
          
          
        
        
          
            
              
              
              
  
           
        
        
          
            
              
              
           
        
       
     
  
        
          
            
          
            
          
        
       
    
      
from
peaktwilight:master
  
      
      
   
  
    
  
  
  
 
  
      
    base: master
Could not load branches
            
              
  
    Branch not found: {{ refName }}
  
            
                
      Loading
              
            Could not load tags
            
            
              Nothing to show
            
              
  
            
                
      Loading
              
            Are you sure you want to change the base?
            Some commits from the old base branch may be removed from the timeline,
            and old review comments may become outdated.
          
          
      
        
          +783
        
        
          β73
        
        
          
        
      
    
  
  
     Draft
                    Changes from 6 commits
      Commits
    
    
            Show all changes
          
          
            69 commits
          
        
        Select commit
          Hold shift + click to select a range
      
      ad713ed
              
                feat: Add configurable heartbeat bar range for status pages
              
              
                peaktwilight 493a5fd
              
                added heartbeat range
              
              
                peaktwilight e1cf10d
              
                fetch aggregated data overengineering?
              
              
                peaktwilight c0f09d9
              
                simplify overengineered aggregated fetches
              
              
                peaktwilight 531faa9
              
                fix linting
              
              
                peaktwilight b441308
              
                lang file fix
              
              
                peaktwilight ebd47f1
              
                refactor: Clean up heartbeat range handling in status page
              
              
                peaktwilight 2c6d410
              
                modularized with a heartbeat range util
              
              
                peaktwilight 8d2e3f1
              
                remove client-side aggregation and add server-side support for it in β¦
              
              
                peaktwilight dbf58b8
              
                try stat tables first, then add fallback if pre-aggregated data fails
              
              
                peaktwilight f47f73d
              
                fix timestamp format calculation according to db
              
              
                peaktwilight e16b6cf
              
                . Number of beats adapts to container width, just like auto mode :)
              
              
                peaktwilight 94adf2c
              
                cleaned up debugging
              
              
                peaktwilight b41d10e
              
                cleaned up empty lines + linting
              
              
                peaktwilight ace9ff2
              
                simplified to day int instead of custom options accross all components.
              
              
                peaktwilight 0dde42e
              
                added validate and other improvements
              
              
                peaktwilight 992bcdb
              
                fix linting
              
              
                peaktwilight 75fc01c
              
                simplifications
              
              
                peaktwilight 9402c0f
              
                rebucketing
              
              
                peaktwilight 804bb39
              
                time calculation & cleanup
              
              
                peaktwilight bf7d6de
              
                Remove unused client-side aggregation logic.
              
              
                peaktwilight 76b9680
              
                bug fix, made beats bars smaller
              
              
                peaktwilight 05c9ec2
              
                fix client-side aggregation as well for dynamic beat bar count
              
              
                peaktwilight 28eae65
              
                change color suggestion to primary
              
              
                peaktwilight 8b2bf8a
              
                fix 30-90 range
              
              
                peaktwilight 1f4c4a0
              
                fix range 30-90 v2
              
              
                peaktwilight 1518fd5
              
                address CommanderStorm's feedback + revert statuspage pr
              
              
                peaktwilight adc362a
              
                rethought beat aggregation system fully server-side
              
              
                peaktwilight c023015
              
                cleanup
              
              
                peaktwilight 6defb5d
              
                refactor status page router into uptime calculator
              
              
                peaktwilight eaace3e
              
                added aggregation tests
              
              
                peaktwilight b6227e7
              
                linting fix
              
              
                peaktwilight 3a5bedd
              
                fix distribute daily data across bucket ranges
              
              
                peaktwilight 100cd62
              
                added more edge case tests to daily data bucketeting
              
              
                peaktwilight 24b6209
              
                lint fix
              
              
                peaktwilight 6c26d32
              
                update uptime component with dynamic range
              
              
                peaktwilight 9cc91c3
              
                npm install timeout?
              
              
                peaktwilight 9e07862
              
                npm install timeout?
              
              
                peaktwilight 5e64226
              
                Update server/routers/status-page-router.js
              
              
                peaktwilight eb47b1f
              
                refactor _24 suffix
              
              
                peaktwilight 88b3cfc
              
                maxbeats simplification
              
              
                peaktwilight 91221b5
              
                keep track of original order instead of iterating through all buckets
              
              
                peaktwilight 5c6cf48
              
                status page router ismplification getData
              
              
                peaktwilight 29e885b
              
                fix minor issues, falsy check & unnecessary defaults & simplify to a β¦
              
              
                peaktwilight dc74bb7
              
                lint fix
              
              
                peaktwilight 28ddaf1
              
                review
              
              
                peaktwilight 59043fb
              
                Update src/components/HeartbeatBar.vue
              
              
                peaktwilight 0439566
              
                Update src/pages/StatusPage.vue
              
              
                peaktwilight 4160045
              
                Update src/components/HeartbeatBar.vue
              
              
                peaktwilight a15e897
              
                updated tests and various improvements
              
              
                peaktwilight 751c92b
              
                lint fix
              
              
                peaktwilight fd0fe64
              
                fix test for scale factor
              
              
                peaktwilight 0028db4
              
                boundaries test fix
              
              
                peaktwilight b907d62
              
                tests clean up
              
              
                peaktwilight 8a5202b
              
                bring back snapshot test
              
              
                peaktwilight 3cecc67
              
                lint fix
              
              
                peaktwilight 1514cbd
              
                test fix
              
              
                peaktwilight d07575b
              
                smallint
              
              
                peaktwilight 2707aa5
              
                remove normalized heartbeat days
              
              
                peaktwilight b121cc7
              
                update language strings w new format
              
              
                peaktwilight 71b73e9
              
                uptime uptime variable location & unnecessary variables
              
              
                peaktwilight 38a4b60
              
                added comment on when it can happen
              
              
                peaktwilight 60d879f
              
                lint fix
              
              
                peaktwilight 49e181e
              
                removed data pt size
              
              
                peaktwilight c780c2a
              
                reverted bucketing as requested & adapted tests
              
              
                peaktwilight 0d2c129
              
                fix zoom out 1080p dissapear
              
              
                peaktwilight c1b2f35
              
                Merge remote-tracking branch 'upstream/master'
              
              
                peaktwilight 8589c63
              
                fix: Ensure heartbeatBarDays defaults to 0 in StatusPage configuration
              
              
                peaktwilight 8d7e213
              
                Simplified with slicing, because resizing and reloading data from theβ¦
              
              
                peaktwilight File filter
Filter by extension
Conversations
          Failed to load comments.   
        
        
          
      Loading
        
  Jump to
        
          Jump to file
        
      
      
          Failed to load files.   
        
        
          
      Loading
        
  Diff view
Diff view
          Some comments aren't visible on the classic Files Changed page.
        
There are no files selected for viewing
        
          
          
            11 changes: 11 additions & 0 deletions
          
          11 
        
  db/knex_migrations/2025-06-14-0000-heartbeat-range-config.js
  
  
      
      
   
        
      
      
    
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| exports.up = function (knex) { | ||
| return knex.schema.alterTable("status_page", function (table) { | ||
| table.string("heartbeat_bar_range").defaultTo("auto"); | ||
| }); | ||
| }; | ||
| 
     | 
||
| exports.down = function (knex) { | ||
| return knex.schema.alterTable("status_page", function (table) { | ||
| table.dropColumn("heartbeat_bar_range"); | ||
| }); | ||
| }; | 
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
      
      Oops, something went wrong.
      
    
  
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -5,19 +5,19 @@ | |
| v-for="(beat, index) in shortBeatList" | ||
| :key="index" | ||
| class="beat-hover-area" | ||
| :class="{ 'empty': (beat === 0) }" | ||
| :class="{ 'empty': (beat === 0 || beat === null || beat.status === null) }" | ||
                
      
                  peaktwilight marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| :style="beatHoverAreaStyle" | ||
| :title="getBeatTitle(beat)" | ||
| > | ||
| <div | ||
| class="beat" | ||
| :class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }" | ||
| :class="{ 'empty': (beat === 0 || beat === null || beat.status === null), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }" | ||
                
       | 
||
| :style="beatStyle" | ||
| /> | ||
| </div> | ||
| </div> | ||
| <div | ||
| v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'" | ||
| v-if="!$root.isMobile && size !== 'small' && shortBeatList.length > 4 && $root.styleElapsedTime !== 'none'" | ||
                
      
                  peaktwilight marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| class="d-flex justify-content-between align-items-center word" :style="timeStyle" | ||
| > | ||
| <div>{{ timeSinceFirstBeat }}</div> | ||
| 
          
            
          
           | 
    @@ -46,6 +46,11 @@ export default { | |
| heartbeatList: { | ||
| type: Array, | ||
| default: null, | ||
| }, | ||
| /** Heartbeat bar range */ | ||
| heartbeatBarRange: { | ||
| type: String, | ||
| default: "auto", | ||
| } | ||
| }, | ||
| data() { | ||
| 
          
            
          
           | 
    @@ -98,6 +103,12 @@ export default { | |
| return []; | ||
| } | ||
| 
     | 
||
| // If heartbeat range is configured (not auto), aggregate by time periods | ||
| if (this.heartbeatBarRange && this.heartbeatBarRange !== "auto") { | ||
| return this.aggregatedBeatList; | ||
| } | ||
| 
     | 
||
| // Original logic for short time ranges | ||
| let placeholders = []; | ||
| 
     | 
||
| let start = this.beatList.length - this.maxBeat; | ||
| 
        
          
        
         | 
    @@ -117,6 +128,100 @@ export default { | |
| return placeholders.concat(this.beatList.slice(start)); | ||
| }, | ||
| 
     | 
||
| aggregatedBeatList() { | ||
| if (!this.beatList || !this.heartbeatBarRange || this.heartbeatBarRange === "auto") { | ||
| return []; | ||
| } | ||
| 
     | 
||
| const now = dayjs(); | ||
| const buckets = []; | ||
| 
     | 
||
| // Parse the range to get total time and determine bucket size | ||
| let totalHours; | ||
| let bucketSize; // in hours | ||
| let totalBuckets = this.maxBeat || 50; // Use maxBeat to determine bucket count | ||
| 
     | 
||
| if (this.heartbeatBarRange.endsWith("h")) { | ||
| totalHours = parseInt(this.heartbeatBarRange); | ||
| } else if (this.heartbeatBarRange.endsWith("d")) { | ||
| const days = parseInt(this.heartbeatBarRange); | ||
| totalHours = days * 24; | ||
| } else { | ||
| // Fallback | ||
| totalHours = 90 * 24; | ||
| } | ||
| 
     | 
||
| // Calculate bucket size to fit the desired number of buckets | ||
| bucketSize = totalHours / totalBuckets; | ||
| 
     | 
||
| // Create time buckets from oldest to newest | ||
| const startTime = now.subtract(totalHours, "hours"); | ||
| for (let i = 0; i < totalBuckets; i++) { | ||
| let bucketStart; | ||
| let bucketEnd; | ||
| if (bucketSize < 1) { | ||
| // Handle sub-hour buckets (minutes) | ||
| const minutes = bucketSize * 60; | ||
| bucketStart = startTime.add(i * minutes, "minutes"); | ||
| bucketEnd = bucketStart.add(minutes, "minutes"); | ||
| } else { | ||
| // Handle hour+ buckets | ||
| bucketStart = startTime.add(i * bucketSize, "hours"); | ||
| bucketEnd = bucketStart.add(bucketSize, "hours"); | ||
| } | ||
| 
     | 
||
| buckets.push({ | ||
| start: bucketStart, | ||
| end: bucketEnd, | ||
| beats: [], | ||
| status: 1, // default to up | ||
| time: bucketEnd.toISOString() | ||
| }); | ||
| } | ||
| 
     | 
||
| // Group heartbeats into buckets | ||
| this.beatList.forEach(beat => { | ||
| const beatTime = dayjs.utc(beat.time).local(); | ||
| const bucket = buckets.find(b => | ||
| (beatTime.isAfter(b.start) || beatTime.isSame(b.start)) && | ||
| (beatTime.isBefore(b.end) || beatTime.isSame(b.end)) | ||
| ); | ||
| if (bucket) { | ||
| bucket.beats.push(beat); | ||
| } | ||
| }); | ||
| 
     | 
||
| // Calculate status for each bucket | ||
| buckets.forEach(bucket => { | ||
| if (bucket.beats.length === 0) { | ||
| bucket.status = null; // no data - will be rendered as empty/grey | ||
| bucket.time = bucket.end.toISOString(); | ||
| } else { | ||
| // If any beat is down, bucket is down | ||
| // If any beat is maintenance, bucket is maintenance | ||
| // Otherwise bucket is up | ||
| const hasDown = bucket.beats.some(b => b.status === 0); | ||
| const hasMaintenance = bucket.beats.some(b => b.status === 3); | ||
| 
     | 
||
| if (hasDown) { | ||
| bucket.status = 0; | ||
| } else if (hasMaintenance) { | ||
| bucket.status = 3; | ||
| } else { | ||
| bucket.status = 1; | ||
| } | ||
| 
     | 
||
| // Use the latest beat time in the bucket | ||
| const latestBeat = bucket.beats.reduce((latest, beat) => | ||
| dayjs(beat.time).isAfter(dayjs(latest.time)) ? beat : latest | ||
| ); | ||
| bucket.time = latestBeat.time; | ||
| } | ||
| }); | ||
| 
     | 
||
| return buckets; | ||
| }, | ||
| 
     | 
||
| wrapStyle() { | ||
| let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 2); | ||
| let leftRight = (((this.beatWidth * this.hoverScale) - this.beatWidth) / 2); | ||
| 
          
            
          
           | 
    @@ -162,6 +267,14 @@ export default { | |
| * @returns {object} The style object containing the CSS properties for positioning the time element. | ||
| */ | ||
| timeStyle() { | ||
| // For aggregated mode, don't use padding-based positioning | ||
| if (this.heartbeatBarRange && this.heartbeatBarRange !== "auto") { | ||
| return { | ||
| "margin-left": "0px", | ||
| }; | ||
| } | ||
| 
     | 
||
| // Original logic for auto mode | ||
| return { | ||
| "margin-left": this.numPadding * (this.beatWidth + this.beatHoverAreaPadding * 2) + "px", | ||
| }; | ||
| 
        
          
        
         | 
    @@ -172,6 +285,22 @@ export default { | |
| * @returns {string} The time elapsed in minutes or hours. | ||
| */ | ||
| timeSinceFirstBeat() { | ||
| // For aggregated beats, calculate from the configured range | ||
| if (this.heartbeatBarRange && this.heartbeatBarRange !== "auto") { | ||
| if (this.heartbeatBarRange.endsWith("h")) { | ||
| const hours = parseInt(this.heartbeatBarRange); | ||
| return hours + "h"; | ||
| } else if (this.heartbeatBarRange.endsWith("d")) { | ||
| const days = parseInt(this.heartbeatBarRange); | ||
| if (days < 2) { | ||
| return (days * 24) + "h"; | ||
| } else { | ||
| return days + "d"; | ||
| } | ||
| } | ||
| } | ||
| 
     | 
||
| // Original logic for auto mode | ||
| const firstValidBeat = this.shortBeatList.at(this.numPadding); | ||
| const minutes = dayjs().diff(dayjs.utc(firstValidBeat?.time), "minutes"); | ||
| if (minutes > 60) { | ||
| 
          
            
          
           | 
    @@ -267,6 +396,18 @@ export default { | |
| * @returns {string} Beat title | ||
| */ | ||
| getBeatTitle(beat) { | ||
| if (beat === 0) { | ||
| return ""; | ||
| } | ||
| 
     | 
||
| // For aggregated beats, show time range and status | ||
| if (beat.beats !== undefined && this.heartbeatBarRange && this.heartbeatBarRange !== "auto") { | ||
| const start = this.$root.datetime(beat.start); | ||
| const end = this.$root.datetime(beat.end); | ||
| const statusText = beat.status === 1 ? "Up" : beat.status === 0 ? "Down" : beat.status === 3 ? "Maintenance" : "No Data"; | ||
| return `${start} - ${end}: ${statusText} (${beat.beats.length} checks)`; | ||
| } | ||
| 
     | 
||
| return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : ""); | ||
| }, | ||
| 
     | 
||
| 
          
            
          
           | 
    ||
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
      
      Oops, something went wrong.
        
    
  
      
      Oops, something went wrong.
        
    
  
  Add this suggestion to a batch that can be applied as a single commit.
  This suggestion is invalid because no changes were made to the code.
  Suggestions cannot be applied while the pull request is closed.
  Suggestions cannot be applied while viewing a subset of changes.
  Only one suggestion per line can be applied in a batch.
  Add this suggestion to a batch that can be applied as a single commit.
  Applying suggestions on deleted lines is not supported.
  You must change the existing code in this line in order to create a valid suggestion.
  Outdated suggestions cannot be applied.
  This suggestion has been applied or marked resolved.
  Suggestions cannot be applied from pending reviews.
  Suggestions cannot be applied on multi-line comments.
  Suggestions cannot be applied while the pull request is queued to merge.
  Suggestion cannot be applied right now. Please check back later.
  
    
  
    
Uh oh!
There was an error while loading. Please reload this page.