@@ -24,6 +24,33 @@ import {
2424 PublicMaintenance ,
2525} from '../../types/status' ;
2626
27+ /**
28+ * Duration thresholds for the 90-day history bars, mirroring the backend's
29+ * `BucketThresholds`. A day goes major only when hard-down time crosses an
30+ * absolute floor OR a fraction of the day's observed signal (whichever is
31+ * larger), and minor likewise for degraded time. Kept in lockstep with
32+ * `ai-horde-service-alerts`'s defaults so the client- and server-side
33+ * classifications agree.
34+ */
35+ const BAR_MAJOR_DOWN_FLOOR_S = 300 ;
36+ const BAR_MAJOR_DOWN_FRACTION = 0.01 ;
37+ const BAR_MINOR_DEGRADED_FLOOR_S = 600 ;
38+ const BAR_MINOR_DEGRADED_FRACTION = 0.05 ;
39+
40+ /** A history day's rendered bar: its colour modifier, label, and flap marker. */
41+ interface DayBar {
42+ /** BEM modifier suffix for the bar: `ok` | `minor` | `major` | `maintenance`. */
43+ modifier : 'ok' | 'minor' | 'major' | 'maintenance' ;
44+ /** transloco key for the bar's status label, e.g. `status.history_level.ok`. */
45+ labelKey : string ;
46+ /**
47+ * True when the day stayed green but still saw some down/degraded time below
48+ * the colouring threshold — rendered as a small marker under the tick so brief
49+ * instability is visible without being escalated to a full outage.
50+ */
51+ flapping : boolean ;
52+ }
53+
2754/** How a status value reads in the UI: a translation key and a CSS modifier. */
2855interface StatusView {
2956 /** transloco key for the human label, e.g. `status.state.operational`. */
@@ -188,13 +215,66 @@ export class StatusComponent {
188215 return this . histories ( ) [ componentId ] ?? [ ] ;
189216 }
190217
191- /** A day's bar modifier: `ok` | `minor` | `major` | `maintenance`. */
192- public historyLevelModifier ( level : number ) : string {
193- return [ 'ok' , 'minor' , 'major' , 'maintenance' ] [ level ] ?? 'ok' ;
218+ /**
219+ * Classify a day's bar from its raw per-day seconds, mirroring the backend's
220+ * duration-weighted thresholds ({@link BAR_MAJOR_DOWN_FLOOR_S} et al).
221+ *
222+ * This is deliberately independent of the backend's `status_level`: if the
223+ * backend ever regresses to a worst-observed rollup again, the page still
224+ * refuses to paint a whole day red over a few seconds of blip. Sub-threshold
225+ * down/degraded time surfaces as {@link DayBar.flapping} (a small marker)
226+ * rather than a coloured bar.
227+ */
228+ public dayBar ( day : PublicHistoryDay ) : DayBar {
229+ const signal =
230+ day . operational_seconds + day . degraded_seconds + day . down_seconds ;
231+ if ( signal === 0 ) {
232+ const modifier = day . maintenance_seconds > 0 ? 'maintenance' : 'ok' ;
233+ return {
234+ modifier,
235+ labelKey : `status.history_level.${ modifier } ` ,
236+ flapping : false ,
237+ } ;
238+ }
239+ const majorCut = Math . max (
240+ BAR_MAJOR_DOWN_FLOOR_S ,
241+ BAR_MAJOR_DOWN_FRACTION * signal ,
242+ ) ;
243+ if ( day . down_seconds >= majorCut ) {
244+ return {
245+ modifier : 'major' ,
246+ labelKey : 'status.history_level.major' ,
247+ flapping : false ,
248+ } ;
249+ }
250+ const minorCut = Math . max (
251+ BAR_MINOR_DEGRADED_FLOOR_S ,
252+ BAR_MINOR_DEGRADED_FRACTION * signal ,
253+ ) ;
254+ if ( day . degraded_seconds >= minorCut ) {
255+ return {
256+ modifier : 'minor' ,
257+ labelKey : 'status.history_level.minor' ,
258+ flapping : false ,
259+ } ;
260+ }
261+ return {
262+ modifier : 'ok' ,
263+ labelKey : 'status.history_level.ok' ,
264+ flapping : day . down_seconds > 0 || day . degraded_seconds > 0 ,
265+ } ;
194266 }
195267
196- /** transloco key for a history bar's status, e.g. `status.history_level.ok`. */
197- public historyLevelLabelKey ( level : number ) : string {
198- return `status.history_level.${ this . historyLevelModifier ( level ) } ` ;
268+ /** Compact human duration for tooltips: `15s`, `4m`, `1h 32m`. */
269+ public formatDuration ( seconds : number ) : string {
270+ if ( seconds >= 3600 ) {
271+ const hours = Math . floor ( seconds / 3600 ) ;
272+ const minutes = Math . round ( ( seconds % 3600 ) / 60 ) ;
273+ return minutes > 0 ? `${ hours } h ${ minutes } m` : `${ hours } h` ;
274+ }
275+ if ( seconds >= 60 ) {
276+ return `${ Math . round ( seconds / 60 ) } m` ;
277+ }
278+ return `${ Math . max ( 0 , Math . round ( seconds ) ) } s` ;
199279 }
200280}
0 commit comments