Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions openhtf/output/servers/station_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,45 @@ def kill_thread():
self.write('Restarting server')


# Station-side hook for relaying operator fault reports. The framework sets this
# to a callable (payload_dict -> None) that forwards the report to an internal
# Halter service (hardware-test-service). The GUI server itself NEVER calls
# external services — it only invokes this injected handler.
_FAULT_REPORT_HANDLER = None


def set_fault_report_handler(handler):
"""Register the callable invoked with the fault-report payload (see above)."""
global _FAULT_REPORT_HANDLER
_FAULT_REPORT_HANDLER = handler


class FaultReportHandler(web_gui_server.CorsRequestHandler):
"""POST endpoint for operator fault reports from the GUI.

Delegates to the framework-provided handler (set via set_fault_report_handler),
which forwards the report to hardware-test-service; this server never calls
external services itself. Returns 501 if no handler is configured. The handler
runs on a thread so it never blocks the IOLoop.
"""

def post(self):
handler = _FAULT_REPORT_HANDLER
if handler is None:
self.set_status(501)
self.finish({'error': 'fault reporting is not configured on this station'})
return
try:
payload = json.loads(self.request.body)
except (ValueError, TypeError):
self.set_status(400)
self.finish({'error': 'invalid JSON body'})
return
threading.Thread(target=handler, args=(payload,), daemon=True).start()
self.set_status(202)
self.finish({'status': 'reported'})


class AttachmentsHandler(BaseTestHandler):
"""GET endpoint for a file attached to a test."""

Expand Down Expand Up @@ -794,6 +833,7 @@ def __init__(
(r'/tests/(?P<test_uid>[\w\d:]+)/phases/(?P<phase_descriptor_id>\d+)/'
'attachments/(?P<attachment_name>.+)', AttachmentsHandler),
(r'/commands/(?P<command>.+)', CommandHandler),
(r'/fault-report', FaultReportHandler),
))

# Optionally enable history from disk.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions openhtf/output/web_gui/dist/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<head><link href="/css/app.2fc68a878b718b468aa7.css" rel="stylesheet"></head><!doctype html>
<head><link href="/css/app.e9c4d879332ab5131a29.css" rel="stylesheet"></head><!doctype html>
<!--
Copyright 2022 Google LLC

Expand All @@ -22,4 +22,4 @@

<base href="/">
<htf-app config="{{ json_encode(config) }}">Loading...</htf-app>
<script type="text/javascript" src="/js/polyfills.2fc68a878b718b468aa7.js"></script><script type="text/javascript" src="/js/vendor.2fc68a878b718b468aa7.js"></script><script type="text/javascript" src="/js/app.2fc68a878b718b468aa7.js"></script>
<script type="text/javascript" src="/js/polyfills.e9c4d879332ab5131a29.js"></script><script type="text/javascript" src="/js/vendor.e9c4d879332ab5131a29.js"></script><script type="text/javascript" src="/js/app.e9c4d879332ab5131a29.js"></script>

This file was deleted.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

This file was deleted.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions openhtf/output/web_gui/src/app/shared/models/test-state.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,24 @@ export enum TestStatus {

export class PlugDescriptor { mro: string[]; }

// A structured outcome detail recorded on the test record (OpenHTF's
// TestRecord.outcome_details). Halter uses this to surface an operator-facing
// fault code + remediation on a failed test; see the operator fault panel in
// test-summary.component.
export interface OutcomeDetail {
code: string;
issue: string; // what went wrong (text before the "What to do:" marker)
whatToDo: string; // remediation steps (text after the marker); '' if none
}

export class TestState {
attachments: Attachment[];
dutId: string;
endTimeMillis: number|null;
fileName: string|null; // This is null for tests *not* from the history.
name: string;
logs: LogRecord[];
outcomeDetails: OutcomeDetail[];
phases: Phase[];
plugDescriptors: {[name: string]: PlugDescriptor};
plugStates: {[name: string]: {}};
Expand Down
26 changes: 24 additions & 2 deletions openhtf/output/web_gui/src/app/stations/station/station-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { LogRecord } from '../../shared/models/log-record.model';
import { Measurement, MeasurementStatus } from '../../shared/models/measurement.model';
import { Phase, PhaseStatus } from '../../shared/models/phase.model';
import { Station } from '../../shared/models/station.model';
import { PlugDescriptor, TestState, TestStatus } from '../../shared/models/test-state.model';
import { OutcomeDetail, PlugDescriptor, TestState, TestStatus } from '../../shared/models/test-state.model';

import { sortByProperty } from '../../shared/util';

Expand Down Expand Up @@ -75,7 +75,7 @@ export interface RawTestRecord {
log_records: RawLogRecord[];
metadata: RawMetadata;
outcome: string;
outcome_details: Array<{}>;
outcome_details: Array<{code: string|number; description: string}>;
phases: RawPhase[];
start_time_millis: number;
station_id: string;
Expand Down Expand Up @@ -162,13 +162,35 @@ export function makeTest(
status = testStateStatusMap[rawState.status];
}

// Structured operator faults recorded on the test record (may be absent on
// older records or running tests). Coerce code to string for display.
// The raw OperatorActionRequired entry that OpenHTF auto-adds for our own
// raise is dropped — the explicit detail already carries the code + steps.
// Each description is split into an "issue" and a "what to do" section at the
// "What to do:" marker so the GUI fault panel can label them.
const faultWhatToDoMarker = 'What to do:';
const outcomeDetails: OutcomeDetail[] =
(rawState.test_record.outcome_details || [])
.filter(detail => `${detail.code}` !== 'OperatorActionRequired')
.map(detail => {
const description = detail.description || '';
const markerIndex = description.indexOf(faultWhatToDoMarker);
const issue =
(markerIndex >= 0 ? description.slice(0, markerIndex) : description).trim();
const whatToDo = markerIndex >= 0 ?
description.slice(markerIndex + faultWhatToDoMarker.length).trim() :
'';
return {code: `${detail.code}`, issue, whatToDo};
});

return new TestState({
attachments,
dutId: rawState.test_record.dut_id,
endTimeMillis: rawState.test_record.end_time_millis || null,
fileName,
logs,
name: rawState.test_record.metadata.test_name,
outcomeDetails,
phases,
plugDescriptors: rawState.plugs.plug_descriptors,
plugStates: rawState.plugs.plug_states,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,50 @@
limitations under the License.
-->

<!--
Operator fault panel: shown when the test record carries structured
outcome_details (Halter records an operator-facing fault code + remediation
there on a failed test). Rendered prominently above the summary so an
operator returning to a failed test sees the code and what to do.
-->
<div
class="htf-layout-widget operator-fault-panel"
*ngIf="test && test.outcomeDetails && test.outcomeDetails.length">
<div class="htf-layout-widget-header operator-fault-header">
<div>TEST FAILED — ACTION REQUIRED</div>
</div>
<div
class="htf-layout-widget-body operator-fault-detail"
*ngFor="let detail of test.outcomeDetails">
<!-- Primary: what the operator should do (what they actually need). -->
<div *ngIf="detail.whatToDo" class="operator-fault-action">
<div class="operator-fault-action-label">What to do</div>
<pre class="operator-fault-action-body">{{ detail.whatToDo }}</pre>
</div>
<!-- No known remediation: let the operator report it to engineering. -->
<div *ngIf="!detail.whatToDo" class="operator-fault-action">
<div class="operator-fault-action-label">Unexpected error</div>
<div class="operator-fault-action-hint">
This isn't a known fault. Report it so engineering can add a fix.
</div>
<button
type="button"
class="htf-rounded-button-grey"
(click)="reportFault(detail)">
Report to engineering
</button>
</div>
<!-- Secondary, de-emphasised: engineering context (code + issue). -->
<div class="operator-fault-issue u-text-color-deemphasize">
<div>
<span class="operator-fault-issue-label">Issue:</span>
<span class="operator-fault-issue-code">{{ detail.code }}</span>
</div>
<pre class="operator-fault-issue-body">{{ detail.issue }}</pre>
</div>
</div>
</div>

<div class="htf-layout-widget" *ngIf="test">

<div class="htf-layout-widget-header">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,60 @@

@import 'vars';

.operator-fault-panel {
border: 2px solid $theme-red;
margin-bottom: $widget-inner-padding;
}

.operator-fault-header {
background: $theme-red;
color: $theme-white;
font-weight: bold;
}

// Primary action — what the operator actually needs. Made prominent.
.operator-fault-action-label {
font-size: 16px;
font-weight: bold;
}

.operator-fault-action-body {
font-family: inherit;
font-size: 15px;
line-height: 1.5;
margin: 4px 0 0;
white-space: pre-wrap;
word-break: break-word;
}

.operator-fault-action-hint {
font-size: 13px;
margin: 4px 0 8px;
}

// Secondary — engineering context, de-emphasised below the action.
.operator-fault-issue {
border-top: 1px solid $border-light-grey;
font-size: 12px;
margin-top: 12px;
padding-top: 8px;
}

.operator-fault-issue-label {
font-weight: bold;
}

.operator-fault-issue-code {
font-family: monospace;
}

.operator-fault-issue-body {
font-family: inherit;
margin: 2px 0 0;
white-space: pre-wrap;
word-break: break-word;
}

.htf-status-indicator {
line-height: 53px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@
*/

import { Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { Http } from '@angular/http';

import { ConfigService } from '../../core/config.service';
import { FlashMessageService } from '../../core/flash-message.service';
import { logLevels } from '../../shared/models/log-record.model';
import { Phase, PhaseStatus } from '../../shared/models/phase.model';
import { TestState, TestStatus } from '../../shared/models/test-state.model';
import { OutcomeDetail, TestState, TestStatus } from '../../shared/models/test-state.model';
import { ProgressBarComponent } from '../../shared/progress-bar.component';
import { getStationBaseUrl } from '../../shared/util';

@Component({
selector: 'htf-test-summary',
Expand All @@ -33,6 +38,41 @@ export class TestSummaryComponent implements OnChanges {
@Input() test: TestState;
@ViewChild(ProgressBarComponent) progressBar: ProgressBarComponent;

constructor(
private http: Http,
private config: ConfigService,
private flashMessage: FlashMessageService) {}

/**
* Report an unexpected (unmapped) fault. POSTs to the station's /fault-report
* relay — which forwards to hardware-test-service (the station GUI never calls
* external services). Sends station + test context plus the error/critical
* logs (the traceback).
*/
reportFault(detail: OutcomeDetail) {
const traceback = this.test.logs
.filter(log => log.level >= logLevels.error)
.map(log => log.message)
.join('\n\n') ||
detail.issue;
// Field names match hardware-test-service's IReportFaultRequestDTO. The
// station relay fills in fixtureNumber (the canonical station id) before
// forwarding, so we only send what the browser actually knows.
const payload = {
fixtureId: this.test.station.label,
testPlan: this.test.name,
dutId: this.test.dutId,
code: detail.code,
issue: detail.issue,
traceback,
};
const baseUrl = getStationBaseUrl(this.config.dashboardEnabled, this.test.station);
this.http.post(`${baseUrl}/fault-report`, JSON.stringify(payload)).subscribe(
() => this.flashMessage.warn('Error reported to engineering.'),
() => this.flashMessage.error(
'Could not report the error — please ping engineering.'));
}

ngOnChanges(changes: SimpleChanges) {
// When we get a new test, animate the progress bar from zero.
if ('test' in changes && this.progressBar) {
Expand Down
Loading