Skip to content

Commit b3ffde1

Browse files
marbindrakonclaude
andcommitted
Add on-condition (RCM) maintenance mode for TBO-tracked components
Operators following Reliability-Centered Maintenance can mark a component as "Maintained On-Condition" when TBO tracking is enabled. This switches the hours-remaining display from red/orange warning colors to a positive blue→teal→green gradient (approaching/at/past TBO), and shows the status badge as blue rather than red when over TBO. Calendar remaining uses a flat informational blue when over TBO — no gradient, to avoid encouraging over-TBO calendar operation the same way hours does. An "On-Condition" label appears in the expanded component detail panel. Service-interval (replacement_critical) warnings are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 20a16ca commit b3ffde1

9 files changed

Lines changed: 184 additions & 19 deletions

File tree

core/export.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def _component_dict(component):
116116
'replacement_hours': component.replacement_hours,
117117
'replacement_days': component.replacement_days,
118118
'tbo_critical': component.tbo_critical,
119+
'on_condition': component.on_condition,
119120
'inspection_critical': component.inspection_critical,
120121
'replacement_critical': component.replacement_critical,
121122
}

core/import_export.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@ def _check_fk(record_type, field, value, valid_set, nullable=True):
670670
replacement_hours=c_data.get('replacement_hours'),
671671
replacement_days=c_data.get('replacement_days'),
672672
tbo_critical=c_data.get('tbo_critical', True),
673+
on_condition=c_data.get('on_condition', False),
673674
inspection_critical=c_data.get('inspection_critical', True),
674675
replacement_critical=c_data.get('replacement_critical', False),
675676
)

core/static/css/app.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,22 @@
653653
font-weight: 500;
654654
}
655655

656+
/* On-condition TBO: inverted/positive coloring (blue approaching TBO, teal at TBO, green past 150%) */
657+
.hours-on-condition {
658+
color: var(--pf-v5-global--info-color--100);
659+
font-weight: 500;
660+
}
661+
662+
.hours-on-condition-over {
663+
color: #009596;
664+
font-weight: 600;
665+
}
666+
667+
.hours-on-condition-extended {
668+
color: var(--pf-v5-global--success-color--100);
669+
font-weight: 600;
670+
}
671+
656672
/* ========================================
657673
Theme Toggle
658674
======================================== */
@@ -1028,6 +1044,18 @@ html.pf-v5-theme-dark .pf-v5-c-label.pf-m-gold { color: var(--pf-v5-global--pa
10281044
background-color: var(--pf-v5-global--danger-color--100, #c9190b);
10291045
}
10301046

1047+
.component-progress-fill.hours-on-condition {
1048+
background-color: var(--pf-v5-global--info-color--100, #06c);
1049+
}
1050+
1051+
.component-progress-fill.hours-on-condition-over {
1052+
background-color: #009596;
1053+
}
1054+
1055+
.component-progress-fill.hours-on-condition-extended {
1056+
background-color: var(--pf-v5-global--success-color--100, #3e8635);
1057+
}
1058+
10311059
.component-fraction-sublabel {
10321060
font-size: 0.78rem;
10331061
opacity: 0.75;

core/static/js/aircraft-detail-components.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function componentsMixin() {
4141
replacement_hours: '',
4242
replacement_days: '',
4343
tbo_critical: true,
44+
on_condition: false,
4445
inspection_critical: true,
4546
replacement_critical: false,
4647
},
@@ -99,6 +100,31 @@ function componentsMixin() {
99100
},
100101

101102
getHoursRemainingClass(component) {
103+
// On-condition: positive color scheme (blue approaching TBO, green past TBO).
104+
// Only applies when TBO is the primary tracked interval (not service replacement).
105+
const useOnCondition = component.on_condition && component.tbo_critical &&
106+
!(component.replacement_critical &&
107+
(component.replacement_hours || component.replacement_days));
108+
if (useOnCondition) {
109+
if (component.tbo_hours) {
110+
const ratio = (component.hours_since_overhaul || 0) / component.tbo_hours;
111+
if (ratio >= 1.5) return 'hours-on-condition-extended'; // > 150% TBO: green
112+
if (ratio >= 1.0) return 'hours-on-condition-over'; // 100–150% TBO: teal
113+
if (ratio >= 0.9) return 'hours-on-condition'; // 90–100% TBO: blue
114+
return '';
115+
}
116+
if (component.tbo_days) {
117+
const calDays = this.getCalendarDaysRemaining(component);
118+
if (calDays !== null) {
119+
const ratio = (component.tbo_days - calDays) / component.tbo_days;
120+
if (ratio >= 1.5) return 'hours-on-condition-extended';
121+
if (ratio >= 1.0) return 'hours-on-condition-over';
122+
if (ratio >= 0.9) return 'hours-on-condition';
123+
}
124+
}
125+
return '';
126+
}
127+
// Standard alert coloring
102128
const remaining = this.calculateHoursRemaining(component);
103129
if (remaining !== 'N/A') {
104130
const hours = parseFloat(remaining);
@@ -173,6 +199,24 @@ function componentsMixin() {
173199
return Math.round((due - now) / (1000 * 60 * 60 * 24));
174200
},
175201

202+
// CSS class for the "Calendar Remaining" value in the expanded detail panel.
203+
// On-condition mode uses a flat informational blue when past TBO (no gradient —
204+
// calendar time is a secondary indicator and we don't want to reward over-TBO
205+
// operation the way the hours gradient does).
206+
getCalendarRemainingClass(component) {
207+
const calDays = this.getCalendarDaysRemaining(component);
208+
if (calDays === null) return '';
209+
const useOnCondition = component.on_condition && component.tbo_critical &&
210+
!(component.replacement_critical && component.replacement_days);
211+
if (useOnCondition && component.tbo_days && calDays <= 0) {
212+
return 'hours-on-condition';
213+
}
214+
if (calDays <= 0) return 'hours-overdue';
215+
if (calDays < 30) return 'hours-critical';
216+
if (calDays < 90) return 'hours-warning';
217+
return '';
218+
},
219+
176220
// Formats a signed number of days as a human-readable duration string.
177221
// Negative values append " over".
178222
formatDuration(days) {
@@ -284,6 +328,7 @@ function componentsMixin() {
284328
replacement_hours: '',
285329
replacement_days: '',
286330
tbo_critical: true,
331+
on_condition: false,
287332
inspection_critical: true,
288333
replacement_critical: false,
289334
};
@@ -313,6 +358,7 @@ function componentsMixin() {
313358
replacement_hours: component.replacement_hours || '',
314359
replacement_days: component.replacement_days || '',
315360
tbo_critical: component.tbo_critical ?? true,
361+
on_condition: component.on_condition ?? false,
316362
inspection_critical: component.inspection_critical ?? true,
317363
replacement_critical: component.replacement_critical ?? false,
318364
};
@@ -342,6 +388,7 @@ function componentsMixin() {
342388
hours_in_service: parseFloat(this.componentForm.hours_in_service) || 0,
343389
hours_since_overhaul: parseFloat(this.componentForm.hours_since_overhaul) || 0,
344390
tbo_critical: this.componentForm.tbo_critical,
391+
on_condition: this.componentForm.on_condition,
345392
inspection_critical: this.componentForm.inspection_critical,
346393
replacement_critical: this.componentForm.replacement_critical,
347394
};
@@ -540,6 +587,11 @@ function componentsMixin() {
540587

541588
getStatusClass(component) {
542589
if (component.status === 'IN-USE') {
590+
// On-condition: suppress red/orange warnings; blue when at/past TBO
591+
if (component.on_condition && component.tbo_critical && component.tbo_hours) {
592+
const ratio = (component.hours_since_overhaul || 0) / component.tbo_hours;
593+
return ratio >= 1.0 ? 'pf-m-blue' : 'pf-m-green';
594+
}
543595
const hoursToTBO = this.calculateHoursToTBO(component);
544596
if (hoursToTBO !== 'N/A') {
545597
const hours = parseFloat(hoursToTBO);

core/templates/aircraft_detail.html

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,7 @@ <h2>Components</h2>
596596
<template x-if="getCalendarDaysRemaining(component) !== null">
597597
<div>
598598
<div style="font-size: 1.1em; font-weight: 600;"
599-
:class="getCalendarDaysRemaining(component) <= 0 ? 'hours-overdue' : ''"
599+
:class="getCalendarRemainingClass(component)"
600600
x-text="formatDuration(getCalendarDaysRemaining(component))"></div>
601601
<div x-show="getDueDate(component)"
602602
x-text="'Due ' + formatDate(getDueDate(component))"
@@ -615,10 +615,17 @@ <h2>Components</h2>
615615
<div class="component-detail-grid" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem;">
616616
<!-- TBO -->
617617
<div>
618-
<span class="pf-v5-c-label pf-m-compact"
619-
:class="component.tbo_critical ? 'pf-m-blue' : 'pf-m-grey'">
620-
TBO
621-
</span>
618+
<div style="display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap;">
619+
<span class="pf-v5-c-label pf-m-compact"
620+
:class="component.tbo_critical ? 'pf-m-blue' : 'pf-m-grey'">
621+
TBO
622+
</span>
623+
<template x-if="component.tbo_critical && component.on_condition">
624+
<span class="pf-v5-c-label pf-m-compact pf-m-cyan" title="Reliability-Centered / On-Condition maintenance — over-TBO operation is intentional">
625+
On-Condition
626+
</span>
627+
</template>
628+
</div>
622629
<template x-if="component.tbo_critical">
623630
<div style="margin-top: 0.3rem; font-size: 0.9em;">
624631
<div x-show="component.tbo_hours" x-text="component.tbo_hours + ' hr'"></div>
@@ -3535,24 +3542,37 @@ <h1 class="pf-v5-c-modal-box__title" x-text="editingComponent ? 'Edit Component'
35353542
</div>
35363543
</div>
35373544

3538-
<!-- TBO Intervals (shown when tbo_critical) -->
3545+
<!-- TBO Intervals + On-Condition (shown when tbo_critical) -->
35393546
<template x-if="componentForm.tbo_critical">
3540-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
3541-
<div class="pf-v5-c-form__group">
3542-
<label class="pf-v5-c-form__label" for="comp-tbo-hours">
3543-
<span class="pf-v5-c-form__label-text">TBO Hours</span>
3544-
</label>
3545-
<div class="pf-v5-c-form__group-control">
3546-
<input id="comp-tbo-hours" class="pf-v5-c-form-control" type="number" min="0" x-model="componentForm.tbo_hours">
3547+
<div>
3548+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
3549+
<div class="pf-v5-c-form__group">
3550+
<label class="pf-v5-c-form__label" for="comp-tbo-hours">
3551+
<span class="pf-v5-c-form__label-text">TBO Hours</span>
3552+
</label>
3553+
<div class="pf-v5-c-form__group-control">
3554+
<input id="comp-tbo-hours" class="pf-v5-c-form-control" type="number" min="0" x-model="componentForm.tbo_hours">
3555+
</div>
3556+
</div>
3557+
<div class="pf-v5-c-form__group">
3558+
<label class="pf-v5-c-form__label" for="comp-tbo-days">
3559+
<span class="pf-v5-c-form__label-text">TBO Days</span>
3560+
</label>
3561+
<div class="pf-v5-c-form__group-control">
3562+
<input id="comp-tbo-days" class="pf-v5-c-form-control" type="number" min="0" x-model="componentForm.tbo_days">
3563+
</div>
35473564
</div>
35483565
</div>
3549-
<div class="pf-v5-c-form__group">
3550-
<label class="pf-v5-c-form__label" for="comp-tbo-days">
3551-
<span class="pf-v5-c-form__label-text">TBO Days</span>
3566+
<div style="margin-top: 0.75rem;">
3567+
<label style="display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer;">
3568+
<input type="checkbox" x-model="componentForm.on_condition" style="margin-top: 0.2rem; flex-shrink: 0;">
3569+
<span>
3570+
<span style="font-weight: 500;">Maintained On-Condition</span>
3571+
<span style="display: block; font-size: 0.85em; color: var(--pf-v5-global--Color--200);">
3572+
Enables RCM/on-condition maintenance mode. Over-TBO hours are shown in blue/green rather than red/orange.
3573+
</span>
3574+
</span>
35523575
</label>
3553-
<div class="pf-v5-c-form__group-control">
3554-
<input id="comp-tbo-days" class="pf-v5-c-form-control" type="number" min="0" x-model="componentForm.tbo_days">
3555-
</div>
35563576
</div>
35573577
</div>
35583578
</template>

docs/user-guide/components.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,55 @@ Adding a Component
6666
this counter reaches the interval.
6767
- **TBO Critical** -- For parts with a Time Between Overhaul limit (engines,
6868
propellers). Set the **TBO Hours** and current **Hours Since Overhaul**.
69+
When TBO Tracking is enabled, an optional **Maintained On-Condition**
70+
checkbox appears (see below).
6971
- **Inspection Critical** -- For parts requiring periodic inspections. Tracked
7072
via the :doc:`inspections` system.
7173

7274
5. Click **Save**.
7375

7476
.. TODO: Screenshot of the Add Component modal with tracking options visible
7577
78+
On-Condition (RCM) Maintenance
79+
-------------------------------
80+
81+
For complex components such as engines and propellers, many operators follow
82+
**Reliability-Centered Maintenance (RCM)** or **on-condition** philosophy:
83+
the risk of a maintenance-induced failure around an overhaul event can exceed
84+
the risk of simply continuing the component in service past its nominal TBO,
85+
provided the component is monitored and shows no signs of deterioration.
86+
87+
When **TBO Tracking** is enabled on a component, you can check
88+
**Maintained On-Condition** to reflect this operating decision.
89+
90+
**Effect on the UI:**
91+
92+
.. list-table::
93+
:header-rows: 1
94+
:widths: 30 70
95+
96+
* - Condition
97+
- Color shown
98+
* - Under 90 % of TBO
99+
- Normal (no highlight)
100+
* - 90–100 % of TBO
101+
- Blue — approaching TBO reference point
102+
* - 100–150 % of TBO
103+
- Teal — operating past TBO on condition
104+
* - Over 150 % of TBO
105+
- Green — running well past TBO on condition
106+
107+
The component badge (IN-USE) changes from the normal red/orange warning to
108+
**blue** once the component passes TBO, and the **On-Condition** label appears
109+
in the component detail panel.
110+
111+
.. note::
112+
113+
On-Condition mode only suppresses TBO-based color warnings. If the
114+
component also has a **Service Interval** (Replacement Critical) configured,
115+
those service-interval warnings continue to use the standard red/orange
116+
alert colors regardless of this setting.
117+
76118
Resetting Service
77119
-----------------
78120

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.11 on 2026-02-25 06:21
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('health', '0024_ad_document'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='component',
15+
name='on_condition',
16+
field=models.BooleanField(default=False),
17+
),
18+
]

health/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class Component(models.Model):
8080
replacement_hours = models.IntegerField(blank=True, null=True)
8181
replacement_days = models.IntegerField(blank=True, null=True)
8282
tbo_critical = models.BooleanField(default=True)
83+
on_condition = models.BooleanField(default=False)
8384
inspection_critical = models.BooleanField(default=True)
8485
replacement_critical = models.BooleanField(default=False)
8586

health/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class Meta:
6565
'replacement_hours',
6666
'replacement_days',
6767
'tbo_critical',
68+
'on_condition',
6869
'inspection_critical',
6970
'replacement_critical',
7071
'components',
@@ -431,6 +432,7 @@ class Meta:
431432
'replacement_hours',
432433
'replacement_days',
433434
'tbo_critical',
435+
'on_condition',
434436
'inspection_critical',
435437
'replacement_critical',
436438
]

0 commit comments

Comments
 (0)