Merge changes I0e2ca130,I12ef64ba,I269b4bb2,Ibb0ba95a,Icd0e13ec

* changes:
  Fetcher-ify 2022 localizer
  Only log CameraImage's at 1 Hz
  Fixup localizer reset positions
  Fix encoder signs/sides for pi localizer
  Add GyroReading message to drivetrain plot
diff --git a/scouting/www/app.ts b/scouting/www/app.ts
index e22fad0..79c1094 100644
--- a/scouting/www/app.ts
+++ b/scouting/www/app.ts
@@ -10,6 +10,15 @@
 export class App {
   tab: Tab = 'Entry';
 
+  constructor() {
+    window.addEventListener('beforeunload', (e) => {
+      // Based on https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
+      // This combination ensures a dialog will be shown on most browsers.
+      e.preventDefault();
+      e.returnValue = '';
+    });
+  }
+
   tabIs(tab: Tab) {
     return this.tab == tab;
   }
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index af0ce97..1fb672f 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -10,7 +10,7 @@
 import ErrorResponse = error_response.scouting.webserver.requests.ErrorResponse;
 
 type Section = 'Team Selection'|'Auto'|'TeleOp'|'Climb'|'Defense'|'Review and Submit'|'Home'
-type Level = 'Low'|'Medium'|'High'|'Transversal'
+type Level = 'Failed'|'Low'|'Medium'|'High'|'Transversal'
 
 @Component({
     selector: 'app-entry',
@@ -38,6 +38,10 @@
         this.proper = !this.proper;
     }
 
+    setFailed() {
+        this.level = 'Failed';
+    }
+
     setLow() {
         this.level = 'Low';
     }
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index e1273c6..257b40a 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -83,6 +83,7 @@
                 <input (click)="setHigh()" type="radio" name="level" id="high"><label for="high">High</label><br>
                 <input (click)="setTransversal()" type="radio" name="level" id="transversal"><label for="transversal">Transversal</label><br>
                 <input (click)="toggleProper()" type="checkbox" id="proper"><label for="proper">~10 seconds to attempt next level?</label>
+                <input (click)="setFailed()" type="radio" name="level" id="failed"><label for="failed">Failed</label><br>
             </form>
         </div>
         <div class="row">