Merge changes I4c9f0071,Iaa5afeac,I7dbea619 into main

* changes:
  Add flashbax support
  Fix documentation bug in dlqr
  Fix NaNs in physics
diff --git a/frc971/control_loops/swerve/physics_debug.py b/frc971/control_loops/swerve/physics_debug.py
index db9ea85..9a89cc3 100644
--- a/frc971/control_loops/swerve/physics_debug.py
+++ b/frc971/control_loops/swerve/physics_debug.py
@@ -10,7 +10,7 @@
 class PhysicsDebug(object):
 
     def wrap(self, python_module):
-        self.swerve_physics = utils.wrap(python_module.swerve_physics)
+        self.swerve_physics = utils.wrap(python_module.swerve_full_dynamics)
         self.contact_patch_velocity = [
             utils.wrap_module(python_module.contact_patch_velocity, i)
             for i in range(4)
@@ -102,7 +102,7 @@
                               [40.0], [0.0]])
 
         def calc_I(t, x):
-            x_goal = numpy.zeros((16, 1))
+            x_goal = numpy.zeros(16)
 
             Kp_steer = 15.0
             Kp_drive = 0.0
@@ -245,8 +245,7 @@
             U_control = numpy.hstack((
                 U_control,
                 numpy.reshape(
-                    calc_I(result.t[i], numpy.reshape(result.y[:, i],
-                                                      (25, 1))),
+                    calc_I(result.t[i], result.y[:, i]),
                     (8, 1),
                 ),
             ))
diff --git a/scouting/deploy/BUILD b/scouting/deploy/BUILD
index 65b33fb..e453b0b 100644
--- a/scouting/deploy/BUILD
+++ b/scouting/deploy/BUILD
@@ -17,15 +17,6 @@
     include_runfiles = True,
     package_dir = "opt/frc971/scouting_server",
     strip_prefix = ".",
-    # The "include_runfiles" attribute creates a runfiles tree as seen from
-    # within the workspace directory. But what we really want is the runfiles
-    # tree as seen from the root of the runfiles tree (i.e. one directory up).
-    # So we work around it by manually adding some symlinks that let us pretend
-    # that we're at the root of the runfiles tree.
-    symlinks = {
-        "org_frc971": ".",
-        "bazel_tools": "external/bazel_tools",
-    },
 )
 
 pkg_tar(
diff --git a/scouting/deploy/postinst b/scouting/deploy/postinst
index be2b170..20f6052 100644
--- a/scouting/deploy/postinst
+++ b/scouting/deploy/postinst
@@ -21,7 +21,7 @@
 
 # Update the timestamps on the files so that web browsers pull the new version.
 # Otherwise users have to explicitly bypass the cache when visiting the site.
-find /opt/frc971/scouting_server/scouting/www/ -type f -exec touch {} +
+find /opt/frc971/scouting_server/scouting/scouting.runfiles/org_frc971/scouting/www/ -type f -exec touch {} +
 
 systemctl daemon-reload
 systemctl enable scouting.service
diff --git a/scouting/deploy/scouting.service b/scouting/deploy/scouting.service
index 2c55676..f5b0431 100644
--- a/scouting/deploy/scouting.service
+++ b/scouting/deploy/scouting.service
@@ -6,8 +6,8 @@
 User=www-data
 Group=www-data
 Type=simple
-WorkingDirectory=/opt/frc971/scouting_server
-Environment=RUNFILES_DIR=/opt/frc971/scouting_server
+WorkingDirectory=/opt/frc971/scouting_server/scouting/scouting.runfiles/org_frc971
+Environment=RUNFILES_DIR=/opt/frc971/scouting_server/scouting/scouting.runfiles
 # Add "julia" to the PATH.
 Environment=PATH=/opt/frc971/julia_runtime/bin:/usr/local/bin:/usr/bin:/bin
 # Use the Julia cache set up by the frc971-scouting-julia package.
diff --git a/scouting/scouting_test.cy.js b/scouting/scouting_test.cy.js
index 2727104..990e69f 100644
--- a/scouting/scouting_test.cy.js
+++ b/scouting/scouting_test.cy.js
@@ -102,7 +102,12 @@
   cy.contains(/Harmony/).click();
 
   clickButton('End Match');
+
+  clickButton('UNDO');
+  clickButton('End Match');
+
   headerShouldBe(teamNumber + ' Review and Submit ');
+
   cy.get('#review_data li')
     .eq(0)
     .should('have.text', ' Started match at position 1 ');
@@ -113,6 +118,8 @@
       'have.text',
       ' Ended Match; stageType: kHARMONY, trapNote: false, spotlight: false '
     );
+  // Ensure that the penalties action is only submitted once.
+  cy.get('#review_data li').contains('Penalties').should('have.length', 1);
 
   clickButton('Submit');
   headerShouldBe(teamNumber + ' Success ');
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index 1a757a0..3c8aba9 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -334,6 +334,9 @@
           break;
         case ActionType.EndMatchAction:
           this.section = 'Endgame';
+          // Also delete the penalty action.
+          this.undoLastAction();
+          break;
         case ActionType.MobilityAction:
           this.mobilityCompleted = false;
           break;
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index 1b3a966..67d9f9f 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -467,6 +467,9 @@
             <span *ngSwitchCase="ActionType.EndAutoPhaseAction">
               Ended auto phase
             </span>
+            <span *ngSwitchCase="ActionType.EndTeleopPhaseAction">
+              Ended teleop phase
+            </span>
             <span *ngSwitchCase="ActionType.EndMatchAction">
               Ended Match; stageType: {{stringifyStageType((action.actionTaken |
               cast: EndMatchActionT).stageType)}}, trapNote: