[moblab] Improve error handling in mobmonitor-ui

Add an error dialog which will show a human readable
diagnosis, the javascript error, and the stack trace.
Also update the 'last updated at' with an error if it's
determined that we have lost connection.

map localhost:9991/ to the new UI, effectively removing
the old UI.

BUG=chromium:823965
TEST=ng test, e2e/run_e2e/sh

Change-Id: I417a00fffecda755be63d304e487a9923722b612
Reviewed-on: https://chromium-review.googlesource.com/992867
Commit-Ready: Matt Mallett <mattmallett@chromium.org>
Tested-by: Matt Mallett <mattmallett@chromium.org>
Reviewed-by: Keith Haddow <haddowk@chromium.org>
diff --git a/src/mobmonitor-ui/src/app/actions/actions.component.spec.ts b/src/mobmonitor-ui/src/app/actions/actions.component.spec.ts
index 908c87c..3b8769d 100644
--- a/src/mobmonitor-ui/src/app/actions/actions.component.spec.ts
+++ b/src/mobmonitor-ui/src/app/actions/actions.component.spec.ts
@@ -2,9 +2,12 @@
 import { By } from '@angular/platform-browser';
 import { MatSnackBar } from '@angular/material';
 import { of } from 'rxjs/observable/of';
+import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
 
 import { MobmonitorRpcService } from '../services/mobmonitor-rpc.service';
 import { ActionsComponent } from './actions.component';
+import { ErrorDisplayService } from '../services/error-display.service';
+import { wrapError } from '../shared/mobmonitor-error';
 
 
 describe('ActionsComponent', () => {
@@ -14,13 +17,16 @@
   beforeEach(async(() => {
     const rpcSpy = jasmine.createSpyObj('MobmonitorRpcService', ['runAction']);
     const snackSpy = jasmine.createSpyObj('MatSnackBar', ['open']);
+    const errorSpy = jasmine.createSpyObj(
+        'ErrorDisplayService', ['openErrorDialog']);
     TestBed.configureTestingModule({
       declarations: [
         ActionsComponent
       ],
       providers: [
         {provide: MobmonitorRpcService, useValue: rpcSpy},
-        {provide: MatSnackBar, useValue: snackSpy}
+        {provide: MatSnackBar, useValue: snackSpy},
+        {provide: ErrorDisplayService, useValue: errorSpy}
       ]
     })
     .compileComponents();
@@ -58,4 +64,18 @@
     expect(rpc.runAction.calls.count()).toBe(1);
     expect(snackBar.open.calls.count()).toBe(1);
   });
+
+  it('should show an error on failed action', () => {
+    const rpc = <jasmine.SpyObj<MobmonitorRpcService>> fixture.debugElement.injector.get(MobmonitorRpcService);
+    const errorDialog = <jasmine.SpyObj<ErrorDisplayService>> fixture.debugElement.injector.get(ErrorDisplayService);
+
+    rpc.runAction.and.returnValue(
+        new ErrorObservable(wrapError({fail: true}, 'This is bad')));
+
+    const button = fixture.debugElement.query(By.css('button'));
+    button.triggerEventHandler('click', null);
+
+    expect(rpc.runAction.calls.count()).toBe(1);
+    expect(errorDialog.openErrorDialog.calls.count()).toBe(1);
+  });
 });
diff --git a/src/mobmonitor-ui/src/app/actions/actions.component.ts b/src/mobmonitor-ui/src/app/actions/actions.component.ts
index 482fe08..6a7497e 100644
--- a/src/mobmonitor-ui/src/app/actions/actions.component.ts
+++ b/src/mobmonitor-ui/src/app/actions/actions.component.ts
@@ -2,6 +2,7 @@
 import { MatSnackBar } from '@angular/material';
 
 import { MobmonitorRpcService } from '../services/mobmonitor-rpc.service';
+import { ErrorDisplayService } from '../services/error-display.service';
 import { Action } from '../shared/action';
 import { ActionInfo } from '../shared/action-info';
 import { RepairServiceInfo } from '../shared/repair-service-info';
@@ -18,7 +19,8 @@
 
   private actionRunning;
 
-  constructor(private rpc: MobmonitorRpcService, public snackBar: MatSnackBar) { }
+  constructor(private rpc: MobmonitorRpcService, public snackBar: MatSnackBar,
+      private errorDisplay: ErrorDisplayService) { }
 
   ngOnInit() {
     this.actionRunning = {};
@@ -29,14 +31,19 @@
 
   onActionClick(action: Action) {
     this.actionRunning[action.action] = true;
-    this.rpc.runAction(action).subscribe(actionWasRun => {
-      if (actionWasRun) {
+    this.rpc.runAction(action).subscribe(
+      actionWasRun => {
+        if (actionWasRun) {
+          this.actionRunning[action.action] = false;
+          this.snackBar.open(`Action ${action.action} complete`, 'OK', {
+            duration: 2000
+          });
+        }
+      },
+      error => {
+        this.errorDisplay.openErrorDialog(error);
         this.actionRunning[action.action] = false;
-        this.snackBar.open(`Action ${action.action} complete`, 'OK', {
-          duration: 2000
-        });
-      }
-    });
+      });
   }
 
 }
diff --git a/src/mobmonitor-ui/src/app/app.component.html b/src/mobmonitor-ui/src/app/app.component.html
index b8c0586..899d123 100644
--- a/src/mobmonitor-ui/src/app/app.component.html
+++ b/src/mobmonitor-ui/src/app/app.component.html
@@ -5,7 +5,10 @@
 </header>
 
 <div class="container">
-  <p>last updated {{updated | date:'medium'}}</p>
+  <p *ngIf="!lostConnection">last updated {{updated | date:'medium'}}</p>
+  <p *ngIf="lostConnection" class="error">
+    failed to update, last updated {{(updated | date:'medium') || 'never'}}
+  </p>
   <a href="/CollectLogs" target="_blank" mat-raised-button color="primary">
     Download Logs
   </a>
diff --git a/src/mobmonitor-ui/src/app/app.component.scss b/src/mobmonitor-ui/src/app/app.component.scss
index 078b770..f41d1a0 100644
--- a/src/mobmonitor-ui/src/app/app.component.scss
+++ b/src/mobmonitor-ui/src/app/app.component.scss
@@ -32,6 +32,10 @@
   margin: 0;
   margin-bottom: 24px;
   @include subtext;
+
+  &.error {
+    color: red;
+  }
 }
 
 a {
diff --git a/src/mobmonitor-ui/src/app/app.component.ts b/src/mobmonitor-ui/src/app/app.component.ts
index c3ab6ab..b407105 100644
--- a/src/mobmonitor-ui/src/app/app.component.ts
+++ b/src/mobmonitor-ui/src/app/app.component.ts
@@ -1,23 +1,51 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Subscription } from 'rxjs/Subscription';
+import { interval } from 'rxjs/observable/interval';
 
 import { MobmonitorRpcService } from './services/mobmonitor-rpc.service';
+import { HealthCheck } from './shared/health-check';
 
 @Component({
   selector: 'mob-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.scss']
 })
-export class AppComponent implements OnInit {
+export class AppComponent implements OnInit, OnDestroy {
 
+  // time of last successful update
   private updated: Date;
 
+  // indicating that UI has lost connection to the backend
+  // essentially in an error state until regular updates begin
+  // streaming in
+  private lostConnection = false;
+
+  private intervalSub: Subscription;
+
   constructor(private rpc: MobmonitorRpcService) {}
 
   ngOnInit() {
     this.updated = undefined;
-    this.rpc.getStatus().subscribe(
-      value => this.updated = new Date()
-    );
+
+    this.rpc.getStatus().subscribe(val => {
+      this.updated = new Date();
+      this.lostConnection = false;
+    });
+
+    // every 10 seconds check if we have some recent data
+    this.intervalSub = interval(10000).subscribe(() => {
+      // check if our latest data is older than 10 seconds
+      const tenSecondsAgo = Date.now() - 10000;
+      if (this.updated === undefined) {
+        this.lostConnection = true;
+      } else if (this.updated.getTime() < tenSecondsAgo) {
+        this.lostConnection = true;
+      }
+    });
+  }
+
+  ngOnDestroy() {
+    this.intervalSub.unsubscribe();
   }
 
 }
diff --git a/src/mobmonitor-ui/src/app/app.module.ts b/src/mobmonitor-ui/src/app/app.module.ts
index 2883646..25c1075 100644
--- a/src/mobmonitor-ui/src/app/app.module.ts
+++ b/src/mobmonitor-ui/src/app/app.module.ts
@@ -9,8 +9,10 @@
 import { AppComponent } from './app.component';
 import { HealthChecksComponent } from './health-checks/health-checks.component';
 import { MobmonitorRpcService } from './services/mobmonitor-rpc.service';
+import { ErrorDisplayService } from './services/error-display.service';
 import { ActionsComponent } from './actions/actions.component';
 import { ActionsParamDialogComponent } from './services/actions-param-dialog/actions-param-dialog.component';
+import { ErrorDisplayDialogComponent } from './services/error-display-dialog/error-display-dialog.component';
 
 
 @NgModule({
@@ -18,7 +20,8 @@
     AppComponent,
     HealthChecksComponent,
     ActionsComponent,
-    ActionsParamDialogComponent
+    ActionsParamDialogComponent,
+    ErrorDisplayDialogComponent
   ],
   imports: [
     BrowserModule,
@@ -33,9 +36,13 @@
     BrowserAnimationsModule
   ],
   entryComponents: [
-    ActionsParamDialogComponent
+    ActionsParamDialogComponent,
+    ErrorDisplayDialogComponent
   ],
-  providers: [MobmonitorRpcService],
+  providers: [
+    MobmonitorRpcService,
+    ErrorDisplayService
+  ],
   bootstrap: [AppComponent]
 })
 export class AppModule { }
diff --git a/src/mobmonitor-ui/src/app/health-checks/health-checks.component.ts b/src/mobmonitor-ui/src/app/health-checks/health-checks.component.ts
index d61e330..c7482f1 100644
--- a/src/mobmonitor-ui/src/app/health-checks/health-checks.component.ts
+++ b/src/mobmonitor-ui/src/app/health-checks/health-checks.component.ts
@@ -17,10 +17,18 @@
 
   ngOnInit() {
     this.rpc.getStatus().subscribe(
-      value => this.healthChecks = value
+      value => this.healthChecks = value,
+      error => {} // can't really do anything with the error
+      // the app component will notify user of lost connection
     );
   }
 
+  /**
+  Get the label that will be printed for a given healthcheck status
+
+  @param healthCheck the healthcheck to get a label for
+  @return a label for the current status of the healthcheck
+  */
   getHealthCheckLabel(healthCheck: HealthCheck): string {
     switch (healthCheck.health) {
       case 'unhealthy':
@@ -33,6 +41,14 @@
     }
   }
 
+  /**
+  Construct an array of action objects from the healthcheck
+
+  @param healthCheck the healthcheck to extract actions from
+  @param checkName the specific check to extract actions for, this is the
+    Action.healthCheck value
+  @return an array of Actions that are available for the specified check
+  */
   getActionsForCheck(healthCheck: HealthCheck, checkName: string): Action[] {
     const check = healthCheck.errors.filter(c => c.name === checkName)[0];
 
diff --git a/src/mobmonitor-ui/src/app/services/actions-param-dialog/actions-param-dialog.component.spec.ts b/src/mobmonitor-ui/src/app/services/actions-param-dialog/actions-param-dialog.component.spec.ts
index f0cf27c..d5be05b 100644
--- a/src/mobmonitor-ui/src/app/services/actions-param-dialog/actions-param-dialog.component.spec.ts
+++ b/src/mobmonitor-ui/src/app/services/actions-param-dialog/actions-param-dialog.component.spec.ts
@@ -19,7 +19,7 @@
 
 // tslint:disable-next-line
 @Component({selector: 'mat-dialog-actions', template: '<ng-content></ng-content>'})
-class MatDialogActionsSubComponent {}
+class MatDialogActionsStubComponent {}
 
 describe('ActionsParamDialogComponent', () => {
   let component: ActionsParamDialogComponent;
@@ -33,7 +33,7 @@
         MatDialogStubComponent,
         MatFormFieldStubComponent,
         MatErrorStubComponent,
-        MatDialogActionsSubComponent
+        MatDialogActionsStubComponent
       ],
       imports : [
         FormsModule
diff --git a/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.html b/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.html
new file mode 100644
index 0000000..c9047ef
--- /dev/null
+++ b/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.html
@@ -0,0 +1,10 @@
+<h2 mat-dialog-title>{{data.message}}</h2>
+<mat-dialog-content>
+  <p>Error Body</p>
+  <textarea id="error-body-text" readonly value="{{bodyText}}"></textarea>
+  <p>Stack Trace</p>
+  <textarea id="error-stacktrace" readonly value="{{data.stacktrace}}"></textarea>
+</mat-dialog-content>
+<mat-dialog-actions>
+  <button mat-button mat-dialog-close>OK</button>
+</mat-dialog-actions>
diff --git a/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.scss b/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.scss
new file mode 100644
index 0000000..c821510
--- /dev/null
+++ b/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.scss
@@ -0,0 +1,9 @@
+p {
+  margin: 0;
+}
+
+textarea {
+  width: 100%;
+  resize: vertical;
+  min-height: 200px;
+}
diff --git a/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.spec.ts b/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.spec.ts
new file mode 100644
index 0000000..fa9da55
--- /dev/null
+++ b/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.spec.ts
@@ -0,0 +1,76 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { Component } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+
+import { ErrorDisplayDialogComponent } from './error-display-dialog.component';
+import { MobmonitorError, wrapError } from '../../shared/mobmonitor-error';
+
+// tslint:disable-next-line
+@Component({selector: 'mat-dialog-content', template: '<ng-content></ng-content>'})
+class MatDialogStubComponent {}
+
+// tslint:disable-next-line
+@Component({selector: 'mat-dialog-actions', template: '<ng-content></ng-content>'})
+class MatDialogActionsStubComponent {}
+
+describe('ErrorDisplayDialogComponent', () => {
+  let component: ErrorDisplayDialogComponent;
+  let fixture: ComponentFixture<ErrorDisplayDialogComponent>;
+  let dialogRefSpy: jasmine.SpyObj<MatDialogRef<ErrorDisplayDialogComponent>>;
+
+  const fakeError = wrapError({
+      plaintains: 'too many',
+      bananas: 'not enough'
+    },
+    'Unhandled fruit exception (not a real error, not a failed test)'
+  );
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [
+        ErrorDisplayDialogComponent,
+        MatDialogStubComponent,
+        MatDialogActionsStubComponent
+      ],
+      providers: [
+        {
+          provide: MatDialogRef,
+          useValue: jasmine.createSpyObj('MatDialogRef', ['close'])
+        },
+        {
+          provide: MAT_DIALOG_DATA,
+          useValue: fakeError
+        }
+      ]
+    })
+    .compileComponents();
+
+    dialogRefSpy = TestBed.get(MatDialogRef);
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ErrorDisplayDialogComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should have an error', () => {
+    expect(component.data).toEqual(fakeError);
+  });
+
+  it('should display the error', () => {
+    const title = fixture.nativeElement.querySelector('h2');
+    expect(title.textContent).toContain(fakeError.message);
+
+    const body = fixture.nativeElement.querySelector('#error-body-text');
+    expect(body.value).toContain('bananas');
+    expect(body.value).toContain('plaintains');
+
+    const stacktrace = fixture.nativeElement.querySelector('#error-stacktrace');
+    expect(stacktrace.value).toContain('at Object.wrapError');
+  });
+});
diff --git a/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.ts b/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.ts
new file mode 100644
index 0000000..c49ddd8
--- /dev/null
+++ b/src/mobmonitor-ui/src/app/services/error-display-dialog/error-display-dialog.component.ts
@@ -0,0 +1,22 @@
+import { Component, OnInit, Inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+
+import { MobmonitorError } from '../../shared/mobmonitor-error';
+
+@Component({
+  selector: 'mob-error-display-dialog',
+  templateUrl: './error-display-dialog.component.html',
+  styleUrls: ['./error-display-dialog.component.scss']
+})
+export class ErrorDisplayDialogComponent implements OnInit {
+
+  private bodyText: string;
+
+  constructor(public dialogRef: MatDialogRef<ErrorDisplayDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: MobmonitorError) { }
+
+  ngOnInit() {
+    this.bodyText = JSON.stringify(this.data.body);
+  }
+
+}
diff --git a/src/mobmonitor-ui/src/app/services/error-display.service.spec.ts b/src/mobmonitor-ui/src/app/services/error-display.service.spec.ts
new file mode 100644
index 0000000..133cab7
--- /dev/null
+++ b/src/mobmonitor-ui/src/app/services/error-display.service.spec.ts
@@ -0,0 +1,41 @@
+import { TestBed, inject } from '@angular/core/testing';
+import { MatDialog } from '@angular/material';
+
+import { ErrorDisplayService } from './error-display.service';
+import { ErrorDisplayDialogComponent } from './error-display-dialog/error-display-dialog.component';
+import { MobmonitorError, wrapError } from '../shared/mobmonitor-error';
+
+describe('ErrorDisplayService', () => {
+  let service: ErrorDisplayService;
+  let dialogSpy: jasmine.SpyObj<MatDialog>;
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [
+        ErrorDisplayService,
+        {
+          provide: MatDialog,
+          useValue: jasmine.createSpyObj('MatDialog', ['open'])
+        }]
+    });
+    dialogSpy = TestBed.get(MatDialog);
+    service = TestBed.get(ErrorDisplayService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+
+  it('should open an error dialog', () => {
+    const error = wrapError({
+      isBad: true,
+      howBad: 'very'
+    }, 'This is an error');
+    service.openErrorDialog(error);
+    expect(dialogSpy.open).toHaveBeenCalledWith(
+      ErrorDisplayDialogComponent, {
+        width: '700px',
+        data: error
+      }
+    );
+  });
+});
diff --git a/src/mobmonitor-ui/src/app/services/error-display.service.ts b/src/mobmonitor-ui/src/app/services/error-display.service.ts
new file mode 100644
index 0000000..86dbdee
--- /dev/null
+++ b/src/mobmonitor-ui/src/app/services/error-display.service.ts
@@ -0,0 +1,26 @@
+import { Injectable } from '@angular/core';
+import { MatDialog } from '@angular/material';
+
+import { ErrorDisplayDialogComponent } from './error-display-dialog/error-display-dialog.component';
+import { MobmonitorError } from '../shared/mobmonitor-error';
+
+@Injectable()
+export class ErrorDisplayService {
+
+  constructor(public dialog: MatDialog) { }
+
+  /**
+  Opens a helpful dialog describing the error you just encountered
+
+  @param error MobmonitorError that will be used to construct the dialog
+    if you need a MobmonitorError, use mobmonitor-error.wrapError to construct
+    one
+  */
+  openErrorDialog(error: MobmonitorError): void {
+    this.dialog.open(ErrorDisplayDialogComponent, {
+      width: '700px',
+      data: error
+    });
+  }
+
+}
diff --git a/src/mobmonitor-ui/src/app/services/mobmonitor-rpc.service.spec.ts b/src/mobmonitor-ui/src/app/services/mobmonitor-rpc.service.spec.ts
index f67c1e9..ff31afa 100644
--- a/src/mobmonitor-ui/src/app/services/mobmonitor-rpc.service.spec.ts
+++ b/src/mobmonitor-ui/src/app/services/mobmonitor-rpc.service.spec.ts
@@ -8,6 +8,7 @@
 import { HealthCheck } from '../shared/health-check';
 import { Action } from '../shared/action';
 import { ActionInfo } from '../shared/action-info';
+import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
 
 
 describe('MobmonitorRpcService', () => {
@@ -162,6 +163,21 @@
     fakeAsyncRpc.ngOnDestroy();
   }));
 
+  it('should get status not die on http error', fakeAsync(() => {
+    httpSpy.get.and.returnValue(new ErrorObservable('http fail'));
+
+    const fakeAsyncRpc = new MobmonitorRpcService(httpSpy, dialogSpy);
+    let response;
+    const sub = fakeAsyncRpc.getStatus().subscribe((value) => {
+      response = value;
+    });
+
+    tick(1000);
+
+    sub.unsubscribe();
+    fakeAsyncRpc.ngOnDestroy();
+  }));
+
   it('should run action', () => {
     const testAction: Action = {
       action: 'testAction',
@@ -239,5 +255,23 @@
     });
   });
 
+  it('should error on fail get action info', () => {
+    const testAction: Action = {
+      action: 'testAction',
+      service: 'testService',
+      healthCheck: 'testCheck'
+    };
+
+    httpSpy.get.and.returnValue(new ErrorObservable({fail: true}));
+    httpSpy.post.and.returnValue(asyncData({}));
+
+    rpc.runAction(testAction).subscribe(
+      didRun => {},
+      error => {
+        expect(error.message).toEqual('Failed to gather action info');
+        expect(error.body).toEqual({fail: true});
+      }
+    );
+  });
 
 });
diff --git a/src/mobmonitor-ui/src/app/services/mobmonitor-rpc.service.ts b/src/mobmonitor-ui/src/app/services/mobmonitor-rpc.service.ts
index 09166d1..4ded274 100644
--- a/src/mobmonitor-ui/src/app/services/mobmonitor-rpc.service.ts
+++ b/src/mobmonitor-ui/src/app/services/mobmonitor-rpc.service.ts
@@ -3,16 +3,17 @@
 import { Observable } from 'rxjs/Observable';
 import { Subject } from 'rxjs/Subject';
 import { Subscription } from 'rxjs/Subscription';
-import { multicast } from 'rxjs/operators';
-import { interval } from 'rxjs/observable/interval';
+import { timer } from 'rxjs/observable/timer';
+import 'rxjs/add/operator/catch';
+import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
+import { Subscriber } from 'rxjs/Subscriber';
 import { MatDialog } from '@angular/material';
-
 import { HealthCheck } from '../shared/health-check';
 import { Action } from '../shared/action';
 import { ActionInfo } from '../shared/action-info';
 import { RepairServiceInfo } from '../shared/repair-service-info';
 import { ActionsParamDialogComponent } from './actions-param-dialog/actions-param-dialog.component';
-import { Subscriber } from 'rxjs/Subscriber';
+import { MobmonitorError, wrapError } from '../shared/mobmonitor-error';
 
 @Injectable()
 export class MobmonitorRpcService implements OnDestroy {
@@ -24,24 +25,30 @@
   // should go out. mainly used to prevent UI from repainting and messing up
   // UX
   private repairing: boolean;
-  private intervalSub: Subscription;
+  private timerSub: Subscription;
 
   constructor(private http: HttpClient, public dialog: MatDialog) {
     // create a singleton that polls for updates to status
     // all components that need the status will be pushed
     // updates and not need to issue extra HTTP requests
     this.getStatusSubject = new Subject<Array<HealthCheck>>();
-    this.intervalSub = interval(1000).subscribe(() => {
+    this.timerSub = timer(50, 1000).subscribe(() => {
       if (this.repairing) { return; }
-      this.http.get('/GetStatus').subscribe(data => {
-        this.getStatusSubject.next(this.handleGetStatus(data));
-      });
+      this.http.get('/GetStatus')
+        .subscribe(data => {
+          this.getStatusSubject.next(this.handleGetStatus(data));
+        },
+        error => {
+          // emitting an error will kill the subject and make it unable
+          // to emit any new events if the connection is re-established
+          // so just handle the error on the subscriber side by detecting
+          // a time delta
+        });
     });
   }
 
-
   ngOnDestroy() {
-    this.intervalSub.unsubscribe();
+    this.timerSub.unsubscribe();
   }
 
   /**
@@ -54,6 +61,7 @@
     return this.getStatusSubject.asObservable();
   }
 
+
   /**
   formats an http GetStatus response into a HealthCheck object
 
@@ -114,6 +122,10 @@
         } else {
           this.runActionWithoutParams(action, actionInfo, observer);
         }
+      },
+      error => {
+        this.repairing = false;
+        observer.error(wrapError(error, 'Failed to gather action info'));
       });
     });
 
@@ -147,10 +159,15 @@
           this.repairing = false;
           observer.next(true);
         });
+      // user did not finish the form
       } else {
         this.repairing = false;
         observer.next(false);
       }
+    },
+    error => {
+      this.repairing = false;
+      observer.error(wrapError(error, 'Failed to repair service'));
     });
   }
 
@@ -173,6 +190,10 @@
     this.repairService(repairServiceInfo).subscribe(() => {
       this.repairing = false;
       observer.next(true);
+    },
+    error => {
+      this.repairing = false;
+      observer.error(wrapError(error, 'Failed to repair service'));
     });
   }
 
diff --git a/src/mobmonitor-ui/src/app/shared/mobmonitor-error.ts b/src/mobmonitor-ui/src/app/shared/mobmonitor-error.ts
new file mode 100644
index 0000000..7749055
--- /dev/null
+++ b/src/mobmonitor-ui/src/app/shared/mobmonitor-error.ts
@@ -0,0 +1,21 @@
+export interface MobmonitorError {
+    message: string;
+    body: any;
+    stacktrace: string;
+}
+
+/**
+Wraps an arbitrary type of error in a Mobmonitor error
+
+@param error error to wrap
+@param message user friendly message
+@return a MobmonitorError object
+*/
+export function wrapError(error: any, message: string): MobmonitorError {
+  const err = new Error();
+  return {
+    body: error,
+    message,
+    stacktrace: err.stack
+  };
+}
diff --git a/src/mobmonitor/mobmonitor.py b/src/mobmonitor/mobmonitor.py
index 7b1f776..f1e11d7 100644
--- a/src/mobmonitor/mobmonitor.py
+++ b/src/mobmonitor/mobmonitor.py
@@ -42,7 +42,8 @@
   @cherrypy.expose
   def index(self):
     """Presents a welcome message."""
-    return open(os.path.join(self.staticdir, 'templates', 'index.html'))
+    raise cherrypy.HTTPRedirect('/static/index.html')
+
 
   @cherrypy.expose
   def GetServiceList(self):