Merge "Add climber wpilib interface"
diff --git a/BUILD b/BUILD
index 2c99188..219495d 100644
--- a/BUILD
+++ b/BUILD
@@ -52,6 +52,8 @@
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response //scouting/webserver/requests/messages:submit_notes_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response //scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting //scouting/webserver/requests/messages:request_2023_data_scouting_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2024_data_scouting_response //scouting/webserver/requests/messages:request_2024_data_scouting_response_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2024_data_scouting //scouting/webserver/requests/messages:request_2024_data_scouting_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team_response //scouting/webserver/requests/messages:request_matches_for_team_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team //scouting/webserver/requests/messages:request_matches_for_team_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team_response //scouting/webserver/requests/messages:request_notes_for_team_response_go_fbs
@@ -68,12 +70,16 @@
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response //scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions //scouting/webserver/requests/messages:submit_actions_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions_response //scouting/webserver/requests/messages:submit_actions_response_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_2024_actions //scouting/webserver/requests/messages:submit_2024_actions_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_2024_actions_response //scouting/webserver/requests/messages:submit_2024_actions_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule //scouting/webserver/requests/messages:submit_shift_schedule_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule_response //scouting/webserver/requests/messages:submit_shift_schedule_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking //scouting/webserver/requests/messages:submit_driver_ranking_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking_response //scouting/webserver/requests/messages:submit_driver_ranking_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting //scouting/webserver/requests/messages:delete_2023_data_scouting_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting_response //scouting/webserver/requests/messages:delete_2023_data_scouting_response_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2024_data_scouting //scouting/webserver/requests/messages:delete_2024_data_scouting_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2024_data_scouting_response //scouting/webserver/requests/messages:delete_2024_data_scouting_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_pit_image //scouting/webserver/requests/messages:submit_pit_image_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_pit_image_response //scouting/webserver/requests/messages:submit_pit_image_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_pit_images //scouting/webserver/requests/messages:request_pit_images_go_fbs
diff --git a/aos/analysis/in_process_plotter.cc b/aos/analysis/in_process_plotter.cc
index a2ed68c..88591b3 100644
--- a/aos/analysis/in_process_plotter.cc
+++ b/aos/analysis/in_process_plotter.cc
@@ -2,11 +2,11 @@
 
 #include "aos/configuration.h"
 
-namespace frc971::analysis {
+namespace aos::analysis {
 
 namespace {
-const char *kDataPath = "frc971/analysis/cpp_plot";
-const char *kConfigPath = "frc971/analysis/plotter.json";
+const char *kDataPath = "aos/analysis/cpp_plot";
+const char *kConfigPath = "aos/analysis/plotter.json";
 }  // namespace
 
 Plotter::Plotter()
@@ -167,4 +167,4 @@
   next_top_ = 0;
 }
 
-}  // namespace frc971::analysis
+}  // namespace aos::analysis
diff --git a/aos/analysis/in_process_plotter.h b/aos/analysis/in_process_plotter.h
index fdfde8c..0d00deb 100644
--- a/aos/analysis/in_process_plotter.h
+++ b/aos/analysis/in_process_plotter.h
@@ -1,5 +1,5 @@
-#ifndef FRC971_ANALYSIS_IN_PROCESS_PLOTTER_H_
-#define FRC971_ANALYSIS_IN_PROCESS_PLOTTER_H_
+#ifndef AOS_ANALYSIS_IN_PROCESS_PLOTTER_H_
+#define AOS_ANALYSIS_IN_PROCESS_PLOTTER_H_
 
 #include <vector>
 
@@ -7,7 +7,7 @@
 #include "aos/events/simulated_event_loop.h"
 #include "aos/network/web_proxy.h"
 
-namespace frc971::analysis {
+namespace aos::analysis {
 
 // This class wraps the WebProxy class to provide a convenient C++ interface to
 // dynamically generate plots.
@@ -95,5 +95,5 @@
   std::vector<ColorWheelColor> color_wheel_;
 };
 
-}  // namespace frc971::analysis
-#endif  // FRC971_ANALYSIS_IN_PROCESS_PLOTTER_H_
+}  // namespace aos::analysis
+#endif  // AOS_ANALYSIS_IN_PROCESS_PLOTTER_H_
diff --git a/aos/analysis/in_process_plotter_demo.cc b/aos/analysis/in_process_plotter_demo.cc
index a02451a..6141d6a 100644
--- a/aos/analysis/in_process_plotter_demo.cc
+++ b/aos/analysis/in_process_plotter_demo.cc
@@ -8,7 +8,7 @@
 // each plot.
 int main(int argc, char *argv[]) {
   aos::InitGoogle(&argc, &argv);
-  frc971::analysis::Plotter plotter;
+  aos::analysis::Plotter plotter;
   plotter.Title("TITLE!");
   plotter.AddFigure("Fig Foo");
   plotter.ShareXAxis(true);
diff --git a/aos/analysis/plot_data.fbs b/aos/analysis/plot_data.fbs
index 641cd6e..20689c5 100644
--- a/aos/analysis/plot_data.fbs
+++ b/aos/analysis/plot_data.fbs
@@ -3,7 +3,7 @@
 // the data to plot is all packaged within a single Plot message. Each Plot
 // message will correspond to a single view/tab on the web-page, and can have
 // multiple figures, each of which can have multiple lines.
-namespace frc971.analysis;
+namespace aos.analysis;
 
 // Position within the web-page to plot a figure at. [0, 0] will be the upper
 // left corner of the allowable places where plots can be put, and should
diff --git a/aos/analysis/plot_data_utils.ts b/aos/analysis/plot_data_utils.ts
index 25131fb..0153cdb 100644
--- a/aos/analysis/plot_data_utils.ts
+++ b/aos/analysis/plot_data_utils.ts
@@ -1,5 +1,5 @@
 // Provides a plot which handles plotting the plot defined by a
-// frc971.analysis.Plot message.
+// aos.analysis.Plot message.
 import {Plot as PlotFb} from './plot_data_generated';
 import {MessageHandler, TimestampedMessage} from '../../aos/network/www/aos_plotter';
 import {ByteBuffer} from 'flatbuffers';
@@ -28,7 +28,7 @@
   parentDiv.appendChild(plotDiv);
 
   conn.addReliableHandler(
-      '/analysis', 'frc971.analysis.Plot', (data: Uint8Array, time: number) => {
+      '/analysis', 'aos.analysis.Plot', (data: Uint8Array, time: number) => {
         const plotFb = PlotFb.getRootAsPlot(new ByteBuffer(data));
         const name = (!plotFb.title()) ? 'Plot ' + plots.size : plotFb.title();
         const div = document.createElement('div');
diff --git a/aos/analysis/plotter_config.json b/aos/analysis/plotter_config.json
index fef4a50..2ebde74 100644
--- a/aos/analysis/plotter_config.json
+++ b/aos/analysis/plotter_config.json
@@ -2,7 +2,7 @@
   "channels": [
     {
       "name": "/analysis",
-      "type": "frc971.analysis.Plot",
+      "type": "aos.analysis.Plot",
       "max_size": 1000000000
     }
   ],
diff --git a/aos/analysis/py_log_reader.cc b/aos/analysis/py_log_reader.cc
index 57d57d0..4a97a0f 100644
--- a/aos/analysis/py_log_reader.cc
+++ b/aos/analysis/py_log_reader.cc
@@ -27,7 +27,7 @@
 #include "aos/init.h"
 #include "aos/json_to_flatbuffer.h"
 
-namespace frc971::analysis {
+namespace aos::analysis {
 namespace {
 
 // All the data corresponding to a single message.
@@ -299,8 +299,8 @@
 }
 
 }  // namespace
-}  // namespace frc971::analysis
+}  // namespace aos::analysis
 
 PyMODINIT_FUNC PyInit_py_log_reader(void) {
-  return frc971::analysis::InitModule();
+  return aos::analysis::InitModule();
 }
diff --git a/aos/events/logging/timestamp_plot.cc b/aos/events/logging/timestamp_plot.cc
index 8aee90d..5b5cdc2 100644
--- a/aos/events/logging/timestamp_plot.cc
+++ b/aos/events/logging/timestamp_plot.cc
@@ -5,7 +5,7 @@
 #include "aos/init.h"
 #include "aos/util/file.h"
 
-using frc971::analysis::Plotter;
+using aos::analysis::Plotter;
 
 DEFINE_bool(all, false, "If true, plot *all* the nodes at once");
 DEFINE_bool(bounds, false, "If true, plot the noncausal bounds too.");
diff --git a/aos/flatbuffers/base.h b/aos/flatbuffers/base.h
index 87e07c5..f99dbb8 100644
--- a/aos/flatbuffers/base.h
+++ b/aos/flatbuffers/base.h
@@ -277,6 +277,19 @@
   size_t allocated_size_ = 0;
 };
 
+// Allocates and owns a fixed-size memory buffer on the stack.
+//
+// This provides a convenient Allocator for use with the aos::fbs::Builder
+// in realtime code instead of trying to use the VectorAllocator.
+template <std::size_t N>
+class FixedStackAllocator : public SpanAllocator {
+ public:
+  FixedStackAllocator() : SpanAllocator({buffer_, sizeof(buffer_)}) {}
+
+ private:
+  uint8_t buffer_[N];
+};
+
 namespace internal {
 std::ostream &DebugBytes(std::span<const uint8_t> span, std::ostream &os);
 inline void ClearSpan(std::span<uint8_t> span) {
diff --git a/aos/flatbuffers/static_flatbuffers_test.cc b/aos/flatbuffers/static_flatbuffers_test.cc
index 2acc3c8..fd95db3 100644
--- a/aos/flatbuffers/static_flatbuffers_test.cc
+++ b/aos/flatbuffers/static_flatbuffers_test.cc
@@ -215,7 +215,8 @@
     TestMemory(builder.buffer());
   }
   {
-    // aos::FixedAllocator allocator(TestTableStatic::kUnalignedBufferSize);
+    // aos::FixedAllocator
+    // allocator(TestTableStatic::kUnalignedBufferSize);
     aos::fbs::VectorAllocator allocator;
     Builder<TestTableStatic> builder(&allocator);
     TestTableStatic *object = builder.get();
@@ -796,17 +797,17 @@
 // Try to cover ~all supported scalar/flatbuffer types using JSON convenience
 // functions.
 TEST_F(StaticFlatbuffersTest, FlatbufferTypeCoverage) {
-  VerifyJson<frc971::testing::ConfigurationStatic>("{\n\n}");
+  VerifyJson<aos::testing::ConfigurationStatic>("{\n\n}");
   std::string populated_config =
       aos::util::ReadFileToStringOrDie(aos::testing::ArtifactPath(
           "aos/flatbuffers/test_dir/type_coverage.json"));
   // Get rid of a pesky new line.
   populated_config = populated_config.substr(0, populated_config.size() - 1);
-  VerifyJson<frc971::testing::ConfigurationStatic>(populated_config);
+  VerifyJson<aos::testing::ConfigurationStatic>(populated_config);
 
   // And now play around with mutating the buffer.
-  Builder<frc971::testing::ConfigurationStatic> builder =
-      aos::JsonToStaticFlatbuffer<frc971::testing::ConfigurationStatic>(
+  Builder<aos::testing::ConfigurationStatic> builder =
+      aos::JsonToStaticFlatbuffer<aos::testing::ConfigurationStatic>(
           populated_config);
   ASSERT_TRUE(builder.Verify());
   builder.get()->clear_foo_float();
@@ -987,4 +988,58 @@
                   "random access iterator.");
   }
 }
+
+// Confirm that we can use the FixedStackAllocator
+TEST_F(StaticFlatbuffersTest, FixedStackAllocator) {
+  aos::fbs::FixedStackAllocator<Builder<TestTableStatic>::kBufferSize>
+      allocator;
+  Builder<TestTableStatic> builder(&allocator);
+  TestTableStatic *object = builder.get();
+  object->set_scalar(123);
+  {
+    auto vector = object->add_vector_of_scalars();
+    ASSERT_TRUE(vector->emplace_back(4));
+    ASSERT_TRUE(vector->emplace_back(5));
+  }
+  {
+    auto string = object->add_string();
+    string->SetString("Hello, World!");
+  }
+  {
+    auto vector_of_strings = object->add_vector_of_strings();
+    auto sub_string = CHECK_NOTNULL(vector_of_strings->emplace_back());
+    ASSERT_TRUE(sub_string->emplace_back('D'));
+  }
+  { object->set_substruct({971, 254}); }
+  {
+    auto subtable = object->add_subtable();
+    subtable->set_foo(1234);
+  }
+  {
+    auto vector = object->add_vector_of_structs();
+    ASSERT_TRUE(vector->emplace_back({48, 67}));
+    ASSERT_TRUE(vector->emplace_back({118, 148}));
+    ASSERT_TRUE(vector->emplace_back({971, 973}));
+    // Max vector size is three; this should fail.
+    ASSERT_FALSE(vector->emplace_back({1114, 2056}));
+    // We don't have any extra space available.
+    ASSERT_FALSE(vector->reserve(4));
+    ASSERT_FALSE(vector->emplace_back({1114, 2056}));
+  }
+  {
+    auto vector = object->add_vector_of_tables();
+    auto subobject = vector->emplace_back();
+    subobject->set_foo(222);
+  }
+  {
+    auto subtable = object->add_included_table();
+    subtable->set_foo(included::TestEnum::B);
+  }
+  ASSERT_TRUE(builder.AsFlatbufferSpan().Verify());
+  VLOG(1) << aos::FlatbufferToJson(builder.AsFlatbufferSpan(),
+                                   {.multi_line = true});
+  VLOG(1) << AnnotateBinaries(test_schema_, builder.buffer());
+  TestMemory(builder.buffer());
+}
+
 }  // namespace aos::fbs::testing
diff --git a/aos/flatbuffers/test_dir/type_coverage.fbs b/aos/flatbuffers/test_dir/type_coverage.fbs
index d364fd5..96f6890 100644
--- a/aos/flatbuffers/test_dir/type_coverage.fbs
+++ b/aos/flatbuffers/test_dir/type_coverage.fbs
@@ -3,7 +3,7 @@
 // other sources.
 
 // Use a namespace that has no overlap with the aos::fbs namespace of the underlying code.
-namespace frc971.testing;
+namespace aos.testing;
 
 enum BaseType : byte {
     None,
diff --git a/frc971/can_logger/can_logger.cc b/frc971/can_logger/can_logger.cc
index ad885de..d7c2df7 100644
--- a/frc971/can_logger/can_logger.cc
+++ b/frc971/can_logger/can_logger.cc
@@ -7,6 +7,8 @@
                      std::string_view interface_name)
     : fd_(socket(PF_CAN, SOCK_RAW | SOCK_NONBLOCK, CAN_RAW)),
       frames_sender_(event_loop->MakeSender<CanFrame>(channel_name)) {
+  // TOOD(max): Figure out a proper priority
+  event_loop->SetRuntimeRealtimePriority(10);
   struct ifreq ifr;
   strcpy(ifr.ifr_name, interface_name.data());
   PCHECK(ioctl(fd_.get(), SIOCGIFINDEX, &ifr) == 0)
diff --git a/frc971/control_loops/drivetrain/spline_test.cc b/frc971/control_loops/drivetrain/spline_test.cc
index fcd0030..b47f4ac 100644
--- a/frc971/control_loops/drivetrain/spline_test.cc
+++ b/frc971/control_loops/drivetrain/spline_test.cc
@@ -22,7 +22,7 @@
  public:
   static void SetUpTestSuite() {
     if (FLAGS_plot) {
-      plotter_ = std::make_unique<analysis::Plotter>();
+      plotter_ = std::make_unique<aos::analysis::Plotter>();
     }
   }
 
@@ -52,14 +52,14 @@
     }
   }
 
-  static std::unique_ptr<analysis::Plotter> plotter_;
+  static std::unique_ptr<aos::analysis::Plotter> plotter_;
 
   ::Eigen::Matrix<double, 2, 4> control_points_;
   NSpline<4> spline4_;
   NSpline<6> spline6_;
 };
 
-std::unique_ptr<analysis::Plotter> SplineTest::plotter_;
+std::unique_ptr<aos::analysis::Plotter> SplineTest::plotter_;
 
 // Tests that the derivitives of xy integrate back up to the position.
 TEST_F(SplineTest, XYIntegral) {
diff --git a/frc971/imu_fdcan/Dual_IMU/Core/BUILD b/frc971/imu_fdcan/Dual_IMU/Core/BUILD
index 1982395..9ebb287 100644
--- a/frc971/imu_fdcan/Dual_IMU/Core/BUILD
+++ b/frc971/imu_fdcan/Dual_IMU/Core/BUILD
@@ -43,6 +43,9 @@
         "-Wno-unused-but-set-variable",
     ],
     includes = ["Inc"],
+    linkopts = [
+        "-u _printf_float",
+    ],
     deps = [
         ":startup",
         "//frc971/imu_fdcan/Dual_IMU/Drivers/STM32G4xx_HAL_Driver:hal_driver",
diff --git a/frc971/imu_fdcan/README.md b/frc971/imu_fdcan/README.md
index 95db097..8e82f73 100644
--- a/frc971/imu_fdcan/README.md
+++ b/frc971/imu_fdcan/README.md
@@ -22,4 +22,9 @@
 2)  To change code:
     * The main code lives in [`Dual_IMU/Core/Src`](/Dual_IMU/Core/Src/). Make sure your changes happen inside sections marked `/* USER CODE BEGIN ... */` `/* USER CODE END ... */`. Code outside these markers will be overwritten by CubeIDE when generating code after changes to the `.ioc` file.
 3) Build + Run:
-    * Open CubeIDE GUI to build, debug, or run.
\ No newline at end of file
+    * Option 1: Open CubeIDE GUI to build, debug, or run.
+    * Option 2: 
+        1) SSH onto the build server. 
+        2) Run `bazel build -c opt --config=cortex-m4f-imu //frc971/imu_fdcan/Dual_IMU/Core:main.elf`. The output .elf file should be in bazel-bin/frc971/imu_fdcan/Dual_IMU/Core.
+        3) (If deploying code locally) Move file to local directory. For example: `scp <username>@build.frc971.org:<path/to/main.elf> <local/path/to/save/file/`. A good spot to put this locally is ./Dual_IMU/Debug/.
+        3) Open CubeProgrammer. Click the + tab next to "Device memory". Select the generated elf file. Click "Download".
\ No newline at end of file
diff --git a/frc971/orin/gpu_apriltag.cc b/frc971/orin/gpu_apriltag.cc
index 5554703..89625b6 100644
--- a/frc971/orin/gpu_apriltag.cc
+++ b/frc971/orin/gpu_apriltag.cc
@@ -36,7 +36,7 @@
 // Set max age on image for processing at 20 ms.  For 60Hz, we should be
 // processing at least every 16.7ms
 constexpr aos::monotonic_clock::duration kMaxImageAge =
-    std::chrono::milliseconds(20);
+    std::chrono::milliseconds(50);
 
 namespace chrono = std::chrono;
 
@@ -66,6 +66,10 @@
   extrinsics_ = frc971::vision::CameraExtrinsics(calibration_);
   intrinsics_ = frc971::vision::CameraIntrinsics(calibration_);
   dist_coeffs_ = frc971::vision::CameraDistCoeffs(calibration_);
+
+  projection_matrix_ = cv::Mat::zeros(3, 4, CV_64F);
+  intrinsics_.rowRange(0, 3).colRange(0, 3).copyTo(
+      projection_matrix_.rowRange(0, 3).colRange(0, 3));
 }
 
 ApriltagDetector::~ApriltagDetector() {
diff --git a/frc971/vision/extrinsics_calibration.cc b/frc971/vision/extrinsics_calibration.cc
index 5c4ae31..619ea89 100644
--- a/frc971/vision/extrinsics_calibration.cc
+++ b/frc971/vision/extrinsics_calibration.cc
@@ -393,7 +393,7 @@
 
     // TODO<Jim>: Could probably still do a bit more work on naming
     // conventions and what is being shown here
-    frc971::analysis::Plotter plotter;
+    aos::analysis::Plotter plotter;
     plotter.AddFigure("bot (imu) position");
     plotter.AddLine(times_, x, "x_hat(0)");
     plotter.AddLine(times_, y, "x_hat(1)");
diff --git a/scouting/db/db.go b/scouting/db/db.go
index cf1c823..759f7e1 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -68,6 +68,32 @@
 	CollectedBy string
 }
 
+type Stats2024 struct {
+	// This is set to `true` for "pre-scouted" matches. This means that the
+	// match information is unlikely to correspond with an entry in the
+	// `TeamMatch` table.
+	PreScouting bool `gorm:"primaryKey"`
+
+	TeamNumber                                   string `gorm:"primaryKey"`
+	MatchNumber                                  int32  `gorm:"primaryKey"`
+	SetNumber                                    int32  `gorm:"primaryKey"`
+	CompLevel                                    string `gorm:"primaryKey"`
+	StartingQuadrant                             int32
+	SpeakerAuto, AmpAuto                         int32
+	NotesDroppedAuto                             int32
+	MobilityAuto                                 bool
+	Speaker, Amp, SpeakerAmplified, AmpAmplified int32
+	NotesDropped                                 int32
+	Penalties                                    int32
+	AvgCycle                                     int64
+	Park, OnStage, Harmony, TrapNote             bool
+
+	// The username of the person who collected these statistics.
+	// "unknown" if submitted without logging in.
+	// Empty if the stats have not yet been collected.
+	CollectedBy string
+}
+
 type Action struct {
 	PreScouting bool   `gorm:"primaryKey"`
 	TeamNumber  string `gorm:"primaryKey"`
@@ -139,7 +165,7 @@
 		return nil, errors.New(fmt.Sprint("Failed to connect to postgres: ", err))
 	}
 
-	err = database.AutoMigrate(&TeamMatch{}, &Shift{}, &Stats2023{}, &Action{}, &PitImage{}, &NotesData{}, &Ranking{}, &DriverRankingData{}, &ParsedDriverRankingData{})
+	err = database.AutoMigrate(&TeamMatch{}, &Shift{}, &Stats2023{}, &Stats2024{}, &Action{}, &PitImage{}, &NotesData{}, &Ranking{}, &DriverRankingData{}, &ParsedDriverRankingData{})
 	if err != nil {
 		database.Delete()
 		return nil, errors.New(fmt.Sprint("Failed to create/migrate tables: ", err))
@@ -210,6 +236,30 @@
 	return result.Error
 }
 
+func (database *Database) AddToStats2024(s Stats2024) error {
+	if !s.PreScouting {
+		matches, err := database.QueryMatchesString(s.TeamNumber)
+		if err != nil {
+			return err
+		}
+		foundMatch := false
+		for _, match := range matches {
+			if match.MatchNumber == s.MatchNumber {
+				foundMatch = true
+				break
+			}
+		}
+		if !foundMatch {
+			return errors.New(fmt.Sprint(
+				"Failed to find team ", s.TeamNumber,
+				" in match ", s.MatchNumber, " in the schedule."))
+		}
+	}
+
+	result := database.Create(&s)
+	return result.Error
+}
+
 func (database *Database) DeleteFromStats(compLevel_ string, matchNumber_ int32, setNumber_ int32, teamNumber_ string) error {
 	var stats2023 []Stats2023
 	result := database.
@@ -218,6 +268,14 @@
 	return result.Error
 }
 
+func (database *Database) DeleteFromStats2024(compLevel_ string, matchNumber_ int32, setNumber_ int32, teamNumber_ string) error {
+	var stats2024 []Stats2024
+	result := database.
+		Where("comp_level = ? AND match_number = ? AND set_number = ? AND team_number = ?", compLevel_, matchNumber_, setNumber_, teamNumber_).
+		Delete(&stats2024)
+	return result.Error
+}
+
 func (database *Database) DeleteFromActions(compLevel_ string, matchNumber_ int32, setNumber_ int32, teamNumber_ string) error {
 	var actions []Action
 	result := database.
@@ -281,6 +339,12 @@
 	return stats2023, result.Error
 }
 
+func (database *Database) ReturnStats2024() ([]Stats2024, error) {
+	var stats2024 []Stats2024
+	result := database.Find(&stats2024)
+	return stats2024, result.Error
+}
+
 func (database *Database) ReturnStats2023ForTeam(teamNumber string, matchNumber int32, setNumber int32, compLevel string, preScouting bool) ([]Stats2023, error) {
 	var stats2023 []Stats2023
 	result := database.
@@ -290,6 +354,15 @@
 	return stats2023, result.Error
 }
 
+func (database *Database) ReturnStats2024ForTeam(teamNumber string, matchNumber int32, setNumber int32, compLevel string, preScouting bool) ([]Stats2024, error) {
+	var stats2024 []Stats2024
+	result := database.
+		Where("team_number = ? AND match_number = ? AND set_number = ? AND comp_level = ? AND pre_scouting = ?",
+			teamNumber, matchNumber, setNumber, compLevel, preScouting).
+		Find(&stats2024)
+	return stats2024, result.Error
+}
+
 func (database *Database) ReturnRankings() ([]Ranking, error) {
 	var rankins []Ranking
 	result := database.Find(&rankins)
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index d7001b8..4acbe12 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -138,6 +138,190 @@
 	}
 }
 
+func TestAddToStats2024DB(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+
+	correct := []Stats2024{
+		Stats2024{
+			PreScouting: false, TeamNumber: "894",
+			MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 4,
+			SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
+			Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
+			NotesDropped: 0, Penalties: 2, TrapNote: true, AvgCycle: 0,
+			Park: true, OnStage: false, Harmony: false, CollectedBy: "emma",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "942",
+			MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 2,
+			SpeakerAuto: 2, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
+			Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
+			NotesDropped: 0, Penalties: 2, TrapNote: true, AvgCycle: 0,
+			Park: true, OnStage: false, Harmony: false, CollectedBy: "harry",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "432",
+			MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 3,
+			SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
+			Speaker: 2, Amp: 1, SpeakerAmplified: 3, AmpAmplified: 0,
+			NotesDropped: 0, Penalties: 0, TrapNote: false, AvgCycle: 0,
+			Park: false, OnStage: true, Harmony: false, CollectedBy: "henry",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "52A",
+			MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 1,
+			SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+			Speaker: 0, Amp: 1, SpeakerAmplified: 2, AmpAmplified: 3,
+			NotesDropped: 2, Penalties: 0, TrapNote: true, AvgCycle: 0,
+			Park: true, OnStage: false, Harmony: false, CollectedBy: "jordan",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "745",
+			MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 2,
+			SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+			Speaker: 5, Amp: 0, SpeakerAmplified: 2, AmpAmplified: 1,
+			NotesDropped: 1, Penalties: 1, TrapNote: true, AvgCycle: 0,
+			Park: true, OnStage: false, Harmony: false, CollectedBy: "taylor",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "934",
+			MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 3,
+			SpeakerAuto: 1, AmpAuto: 3, NotesDroppedAuto: 0, MobilityAuto: true,
+			Speaker: 0, Amp: 3, SpeakerAmplified: 2, AmpAmplified: 2,
+			NotesDropped: 0, Penalties: 3, TrapNote: true, AvgCycle: 0,
+			Park: false, OnStage: false, Harmony: true, CollectedBy: "katie",
+		},
+	}
+
+	matches := []TeamMatch{
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "894"},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 2, TeamNumber: "942"},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "432"},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "52A"},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "B", AlliancePosition: 2, TeamNumber: "745"},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "B", AlliancePosition: 3, TeamNumber: "934"},
+	}
+
+	for _, match := range matches {
+		err := fixture.db.AddToMatch(match)
+		check(t, err, "Failed to add match")
+	}
+
+	for i := 0; i < len(correct); i++ {
+		err := fixture.db.AddToStats2024(correct[i])
+		check(t, err, "Failed to add 2024stats to DB")
+	}
+
+	got, err := fixture.db.ReturnStats2024()
+	check(t, err, "Failed ReturnStats2024()")
+
+	if !reflect.DeepEqual(correct, got) {
+		t.Errorf("Got %#v,\nbut expected %#v.", got, correct)
+	}
+}
+
+func TestInsertPreScoutedStats2024(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+
+	stats := Stats2024{
+		PreScouting: false, TeamNumber: "6344",
+		MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 4,
+		SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
+		Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
+		NotesDropped: 0, Penalties: 2, TrapNote: true, AvgCycle: 0,
+		Park: true, OnStage: false, Harmony: false, CollectedBy: "emma",
+	}
+
+	// Attempt to insert the non-pre-scouted data and make sure it fails.
+	err := fixture.db.AddToStats2024(stats)
+	if err == nil {
+		t.Fatal("Expected error from inserting the stats.")
+	}
+	if err.Error() != "Failed to find team 6344 in match 3 in the schedule." {
+		t.Fatal("Got:", err.Error())
+	}
+
+	// Mark the data as pre-scouting data. It should now succeed.
+	stats.PreScouting = true
+	err = fixture.db.AddToStats2024(stats)
+	check(t, err, "Failed to add prescouted stats to DB")
+}
+
+func TestQueryingStats2024ByTeam(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+
+	stats := []Stats2024{
+		Stats2024{
+			PreScouting: false, TeamNumber: "328A",
+			MatchNumber: 7, SetNumber: 1, CompLevel: "qm", StartingQuadrant: 1,
+			SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
+			Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
+			NotesDropped: 0, Penalties: 2, TrapNote: true, AvgCycle: 0,
+			Park: false, OnStage: true, Harmony: false, CollectedBy: "emma",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "978",
+			MatchNumber: 2, SetNumber: 2, CompLevel: "qm", StartingQuadrant: 4,
+			SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+			Speaker: 1, Amp: 2, SpeakerAmplified: 0, AmpAmplified: 2,
+			NotesDropped: 0, Penalties: 2, TrapNote: true, AvgCycle: 0,
+			Park: true, OnStage: false, Harmony: false, CollectedBy: "emma",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "328A",
+			MatchNumber: 4, SetNumber: 1, CompLevel: "qm", StartingQuadrant: 2,
+			SpeakerAuto: 1, AmpAuto: 1, NotesDroppedAuto: 1, MobilityAuto: true,
+			Speaker: 0, Amp: 1, SpeakerAmplified: 1, AmpAmplified: 5,
+			NotesDropped: 1, Penalties: 0, TrapNote: false, AvgCycle: 0,
+			Park: false, OnStage: false, Harmony: true, CollectedBy: "emma",
+		},
+	}
+
+	matches := []TeamMatch{
+		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "qm",
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "328A"},
+		TeamMatch{MatchNumber: 2, SetNumber: 2, CompLevel: "qm",
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "978"},
+		TeamMatch{MatchNumber: 4, SetNumber: 1, CompLevel: "qm",
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "328A"},
+	}
+
+	for _, match := range matches {
+		err := fixture.db.AddToMatch(match)
+		check(t, err, "Failed to add match")
+	}
+
+	for i := range stats {
+		err := fixture.db.AddToStats2024(stats[i])
+		check(t, err, "Failed to add 2024stats to DB")
+	}
+
+	// Validate that requesting status for a single team gets us the
+	// expected data.
+	statsFor328A, err := fixture.db.ReturnStats2024ForTeam("328A", 7, 1, "qm", false)
+	check(t, err, "Failed ReturnStats2024()")
+
+	if !reflect.DeepEqual([]Stats2024{stats[0]}, statsFor328A) {
+		t.Errorf("Got %#v,\nbut expected %#v.", statsFor328A, stats[0])
+	}
+	// Validate that requesting team data for a non-existent match returns
+	// nothing.
+	statsForMissing, err := fixture.db.ReturnStats2024ForTeam("6344", 9, 1, "qm", false)
+	check(t, err, "Failed ReturnStats2024()")
+
+	if !reflect.DeepEqual([]Stats2024{}, statsForMissing) {
+		t.Errorf("Got %#v,\nbut expected %#v.", statsForMissing, []Stats2024{})
+	}
+}
+
 func TestAddToStats2023DB(t *testing.T) {
 	fixture := createDatabase(t)
 	defer fixture.TearDown()
@@ -505,6 +689,101 @@
 	}
 }
 
+func TestDeleteFromStats2024(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+
+	startingStats := []Stats2024{
+		Stats2024{
+			PreScouting: false, TeamNumber: "345",
+			MatchNumber: 5, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 1,
+			SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
+			Speaker: 1, Amp: 3, SpeakerAmplified: 1, AmpAmplified: 3,
+			NotesDropped: 0, Penalties: 0, TrapNote: false, AvgCycle: 0,
+			Park: false, OnStage: true, Harmony: false, CollectedBy: "bailey",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "645",
+			MatchNumber: 5, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 4,
+			SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+			Speaker: 1, Amp: 2, SpeakerAmplified: 0, AmpAmplified: 1,
+			NotesDropped: 0, Penalties: 2, TrapNote: true, AvgCycle: 0,
+			Park: true, OnStage: false, Harmony: false, CollectedBy: "kate",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "323",
+			MatchNumber: 5, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 2,
+			SpeakerAuto: 1, AmpAuto: 1, NotesDroppedAuto: 1, MobilityAuto: true,
+			Speaker: 0, Amp: 0, SpeakerAmplified: 2, AmpAmplified: 1,
+			NotesDropped: 1, Penalties: 0, TrapNote: false, AvgCycle: 0,
+			Park: true, OnStage: false, Harmony: false, CollectedBy: "tyler",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "542",
+			MatchNumber: 5, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 1,
+			SpeakerAuto: 1, AmpAuto: 1, NotesDroppedAuto: 0, MobilityAuto: false,
+			Speaker: 1, Amp: 2, SpeakerAmplified: 2, AmpAmplified: 1,
+			NotesDropped: 1, Penalties: 0, TrapNote: false, AvgCycle: 0,
+			Park: false, OnStage: false, Harmony: true, CollectedBy: "max",
+		},
+	}
+
+	correct := []Stats2024{
+		Stats2024{
+			PreScouting: false, TeamNumber: "345",
+			MatchNumber: 5, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 1,
+			SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
+			Speaker: 1, Amp: 3, SpeakerAmplified: 1, AmpAmplified: 3,
+			NotesDropped: 0, Penalties: 0, TrapNote: false, AvgCycle: 0,
+			Park: false, OnStage: true, Harmony: false, CollectedBy: "bailey",
+		},
+	}
+
+	originalMatches := []TeamMatch{
+		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "345"},
+		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "645"},
+		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "323"},
+		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
+			Alliance: "B", AlliancePosition: 2, TeamNumber: "542"},
+	}
+
+	// Matches for which we want to delete the stats.
+	matches := []TeamMatch{
+		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
+			TeamNumber: "645"},
+		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
+			TeamNumber: "323"},
+		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
+			TeamNumber: "542"},
+	}
+
+	for _, match := range originalMatches {
+		err := fixture.db.AddToMatch(match)
+		check(t, err, "Failed to add match")
+		fmt.Println("Match has been added : ", match.TeamNumber)
+	}
+
+	for _, stat := range startingStats {
+		err := fixture.db.AddToStats2024(stat)
+		check(t, err, "Failed to add stat")
+	}
+
+	for _, match := range matches {
+		err := fixture.db.DeleteFromStats2024(match.CompLevel, match.MatchNumber, match.SetNumber, match.TeamNumber)
+		check(t, err, "Failed to delete stat2024")
+	}
+
+	got, err := fixture.db.ReturnStats2024()
+	check(t, err, "Failed ReturnStats2024()")
+
+	if !reflect.DeepEqual(correct, got) {
+		t.Errorf("Got %#v,\nbut expected %#v.", got, correct)
+	}
+}
+
 func TestDeleteFromActions(t *testing.T) {
 	fixture := createDatabase(t)
 	defer fixture.TearDown()
@@ -864,6 +1143,74 @@
 	}
 }
 
+func TestReturnStats2024DB(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+
+	correct := []Stats2024{
+		Stats2024{
+			PreScouting: false, TeamNumber: "894",
+			MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 4,
+			SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
+			Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
+			NotesDropped: 0, Penalties: 2, TrapNote: true, AvgCycle: 0,
+			Park: true, OnStage: false, Harmony: false, CollectedBy: "emma",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "942",
+			MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 2,
+			SpeakerAuto: 2, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
+			Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
+			NotesDropped: 0, Penalties: 2, TrapNote: true, AvgCycle: 0,
+			Park: true, OnStage: false, Harmony: false, CollectedBy: "harry",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "432",
+			MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 3,
+			SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
+			Speaker: 2, Amp: 1, SpeakerAmplified: 3, AmpAmplified: 0,
+			NotesDropped: 0, Penalties: 0, TrapNote: false, AvgCycle: 0,
+			Park: false, OnStage: true, Harmony: false, CollectedBy: "henry",
+		},
+		Stats2024{
+			PreScouting: false, TeamNumber: "52A",
+			MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 1,
+			SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+			Speaker: 0, Amp: 1, SpeakerAmplified: 2, AmpAmplified: 3,
+			NotesDropped: 2, Penalties: 0, TrapNote: true, AvgCycle: 0,
+			Park: true, OnStage: false, Harmony: false, CollectedBy: "jordan",
+		},
+	}
+
+	matches := []TeamMatch{
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "894"},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 2, TeamNumber: "942"},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "432"},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "52A"},
+	}
+
+	for _, match := range matches {
+		err := fixture.db.AddToMatch(match)
+		check(t, err, "Failed to add match")
+	}
+
+	for i := 0; i < len(correct); i++ {
+		err := fixture.db.AddToStats2024(correct[i])
+		check(t, err, fmt.Sprint("Failed to add stats ", i))
+	}
+
+	got, err := fixture.db.ReturnStats2024()
+	check(t, err, "Failed ReturnStats2024()")
+
+	if !reflect.DeepEqual(correct, got) {
+		t.Errorf("Got %#v,\nbut expected %#v.", got, correct)
+	}
+}
+
 func TestReturnStats2023DB(t *testing.T) {
 	fixture := createDatabase(t)
 	defer fixture.TearDown()
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
index 7be151e..846414c 100644
--- a/scouting/webserver/requests/BUILD
+++ b/scouting/webserver/requests/BUILD
@@ -10,9 +10,13 @@
         "//scouting/db",
         "//scouting/webserver/requests/messages:delete_2023_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:delete_2023_data_scouting_response_go_fbs",
+        "//scouting/webserver/requests/messages:delete_2024_data_scouting_go_fbs",
+        "//scouting/webserver/requests/messages:delete_2024_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:error_response_go_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_2024_data_scouting_go_fbs",
+        "//scouting/webserver/requests/messages:request_2024_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_go_fbs",
@@ -27,6 +31,8 @@
         "//scouting/webserver/requests/messages:request_pit_images_response_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_2024_actions_go_fbs",
+        "//scouting/webserver/requests/messages:submit_2024_actions_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_actions_go_fbs",
         "//scouting/webserver/requests/messages:submit_actions_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_driver_ranking_go_fbs",
@@ -51,8 +57,11 @@
         "//scouting/db",
         "//scouting/webserver/requests/debug",
         "//scouting/webserver/requests/messages:delete_2023_data_scouting_go_fbs",
+        "//scouting/webserver/requests/messages:delete_2024_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_2024_data_scouting_go_fbs",
+        "//scouting/webserver/requests/messages:request_2024_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_go_fbs",
@@ -66,6 +75,7 @@
         "//scouting/webserver/requests/messages:request_pit_images_response_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_2024_actions_go_fbs",
         "//scouting/webserver/requests/messages:submit_actions_go_fbs",
         "//scouting/webserver/requests/messages:submit_driver_ranking_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_go_fbs",
diff --git a/scouting/webserver/requests/debug/BUILD b/scouting/webserver/requests/debug/BUILD
index 1224710..ff85206 100644
--- a/scouting/webserver/requests/debug/BUILD
+++ b/scouting/webserver/requests/debug/BUILD
@@ -8,8 +8,10 @@
     visibility = ["//visibility:public"],
     deps = [
         "//scouting/webserver/requests/messages:delete_2023_data_scouting_response_go_fbs",
+        "//scouting/webserver/requests/messages:delete_2024_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:error_response_go_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_2024_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_notes_response_go_fbs",
@@ -17,6 +19,7 @@
         "//scouting/webserver/requests/messages:request_notes_for_team_response_go_fbs",
         "//scouting/webserver/requests/messages:request_pit_images_response_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_2024_actions_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_actions_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_driver_ranking_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_response_go_fbs",
diff --git a/scouting/webserver/requests/debug/debug.go b/scouting/webserver/requests/debug/debug.go
index faf4a9d..ed8a1b7 100644
--- a/scouting/webserver/requests/debug/debug.go
+++ b/scouting/webserver/requests/debug/debug.go
@@ -10,8 +10,10 @@
 	"net/http"
 
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2024_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2024_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_notes_response"
@@ -19,6 +21,7 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_pit_images_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_2024_actions_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response"
@@ -127,6 +130,12 @@
 		request_2023_data_scouting_response.GetRootAsRequest2023DataScoutingResponse)
 }
 
+func Request2024DataScouting(server string, requestBytes []byte) (*request_2024_data_scouting_response.Request2024DataScoutingResponseT, error) {
+	return sendMessage[request_2024_data_scouting_response.Request2024DataScoutingResponseT](
+		server+"/requests/request/2024_data_scouting", requestBytes,
+		request_2024_data_scouting_response.GetRootAsRequest2024DataScoutingResponse)
+}
+
 func SubmitNotes(server string, requestBytes []byte) (*submit_notes_response.SubmitNotesResponseT, error) {
 	return sendMessage[submit_notes_response.SubmitNotesResponseT](
 		server+"/requests/submit/submit_notes", requestBytes,
@@ -175,6 +184,12 @@
 		submit_driver_ranking_response.GetRootAsSubmitDriverRankingResponse)
 }
 
+func Submit2024Actions(server string, requestBytes []byte) (*submit_2024_actions_response.Submit2024ActionsResponseT, error) {
+	return sendMessage[submit_2024_actions_response.Submit2024ActionsResponseT](
+		server+"/requests/submit/submit_2024_actions", requestBytes,
+		submit_2024_actions_response.GetRootAsSubmit2024ActionsResponse)
+}
+
 func SubmitActions(server string, requestBytes []byte) (*submit_actions_response.SubmitActionsResponseT, error) {
 	return sendMessage[submit_actions_response.SubmitActionsResponseT](
 		server+"/requests/submit/submit_actions", requestBytes,
@@ -192,3 +207,9 @@
 		server+"/requests/delete/delete_2023_data_scouting", requestBytes,
 		delete_2023_data_scouting_response.GetRootAsDelete2023DataScoutingResponse)
 }
+
+func Delete2024DataScouting(server string, requestBytes []byte) (*delete_2024_data_scouting_response.Delete2024DataScoutingResponseT, error) {
+	return sendMessage[delete_2024_data_scouting_response.Delete2024DataScoutingResponseT](
+		server+"/requests/delete/delete_2024_data_scouting", requestBytes,
+		delete_2024_data_scouting_response.GetRootAsDelete2024DataScoutingResponse)
+}
diff --git a/scouting/webserver/requests/messages/BUILD b/scouting/webserver/requests/messages/BUILD
index 735caab..d0534a51 100644
--- a/scouting/webserver/requests/messages/BUILD
+++ b/scouting/webserver/requests/messages/BUILD
@@ -11,6 +11,8 @@
     "request_all_notes_response",
     "request_2023_data_scouting",
     "request_2023_data_scouting_response",
+    "request_2024_data_scouting",
+    "request_2024_data_scouting_response",
     "submit_notes",
     "submit_notes_response",
     "request_notes_for_team",
@@ -29,8 +31,12 @@
     "submit_driver_ranking_response",
     "submit_actions",
     "submit_actions_response",
+    "submit_2024_actions",
+    "submit_2024_actions_response",
     "delete_2023_data_scouting",
     "delete_2023_data_scouting_response",
+    "delete_2024_data_scouting",
+    "delete_2024_data_scouting_response",
 )
 
 filegroup(
diff --git a/scouting/webserver/requests/messages/delete_2024_data_scouting.fbs b/scouting/webserver/requests/messages/delete_2024_data_scouting.fbs
new file mode 100644
index 0000000..827aa98
--- /dev/null
+++ b/scouting/webserver/requests/messages/delete_2024_data_scouting.fbs
@@ -0,0 +1,10 @@
+namespace scouting.webserver.requests;
+
+table Delete2024DataScouting {
+    comp_level:string (id: 0);
+    match_number:int (id: 1);
+    set_number:int (id: 2);
+    team_number:string (id: 3);
+}
+
+root_type Delete2024DataScouting;
diff --git a/scouting/webserver/requests/messages/delete_2024_data_scouting_response.fbs b/scouting/webserver/requests/messages/delete_2024_data_scouting_response.fbs
new file mode 100644
index 0000000..2dbb81f
--- /dev/null
+++ b/scouting/webserver/requests/messages/delete_2024_data_scouting_response.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table Delete2024DataScoutingResponse {
+    // empty response
+}
+
+root_type Delete2024DataScoutingResponse;
diff --git a/scouting/webserver/requests/messages/request_2024_data_scouting.fbs b/scouting/webserver/requests/messages/request_2024_data_scouting.fbs
new file mode 100644
index 0000000..738aade
--- /dev/null
+++ b/scouting/webserver/requests/messages/request_2024_data_scouting.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table Request2024DataScouting {
+
+}
+
+root_type Request2024DataScouting;
diff --git a/scouting/webserver/requests/messages/request_2024_data_scouting_response.fbs b/scouting/webserver/requests/messages/request_2024_data_scouting_response.fbs
new file mode 100644
index 0000000..d653f04
--- /dev/null
+++ b/scouting/webserver/requests/messages/request_2024_data_scouting_response.fbs
@@ -0,0 +1,37 @@
+namespace scouting.webserver.requests;
+
+table Stats2024 {
+  team_number:string (id: 0);
+  match_number:int (id: 1);
+  set_number:int (id: 18);
+  comp_level:string (id: 19);
+
+  starting_quadrant:int (id: 2);
+  speaker_auto:int (id:3);
+  amp_auto:int (id:4);
+  notes_dropped_auto:int (id: 5);
+  mobility_auto: bool (id: 6);
+
+  speaker:int (id:7);
+  amp:int (id:8);
+  speaker_amplified:int (id:9);
+  amp_amplified:int (id:10);
+  notes_dropped:int (id:11);
+
+  penalties:int (id:12);
+  trap_note:bool (id:13);
+  // Time in nanoseconds.
+  avg_cycle: int64 (id:14);
+  park: bool (id:15);
+  on_stage: bool (id:16);
+  harmony: bool (id:17);
+
+  pre_scouting:bool (id:20);
+  collected_by:string (id:21);
+}
+
+table Request2024DataScoutingResponse {
+    stats_list:[Stats2024] (id:0);
+}
+
+root_type Request2024DataScoutingResponse;
diff --git a/scouting/webserver/requests/messages/submit_2024_actions.fbs b/scouting/webserver/requests/messages/submit_2024_actions.fbs
new file mode 100644
index 0000000..5cb3cbe
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_2024_actions.fbs
@@ -0,0 +1,71 @@
+namespace scouting.webserver.requests;
+
+table StartMatchAction {
+    position:int (id:0);
+}
+
+enum ScoreType: short {
+    kAMP,
+    kAMP_AMPLIFIED,
+    kSPEAKER,
+    kSPEAKER_AMPLIFIED,
+}
+
+table MobilityAction {
+    mobility:bool (id:0);
+}
+
+table PenaltyAction {}
+
+table PickupNoteAction {
+    auto:bool (id:0);
+}
+
+table PlaceNoteAction {
+    score_type:ScoreType (id:0);
+    auto:bool (id:1);
+}
+
+table RobotDeathAction {
+    robot_dead:bool (id:0);
+}
+
+enum StageType: short {
+    kON_STAGE,
+    kPARK,
+    kHARMONY,
+    kMISSING,
+}
+
+table EndMatchAction {
+    stage_type:StageType (id:0);
+    trap_note:bool (id:1);
+}
+
+union ActionType {
+    MobilityAction,
+    StartMatchAction,
+    PickupNoteAction,
+    PlaceNoteAction,
+    PenaltyAction,
+    RobotDeathAction,
+    EndMatchAction
+}
+
+table Action {
+    timestamp:int64 (id:0);
+    action_taken:ActionType (id:2);
+}
+
+table Submit2024Actions {
+    team_number:string (id: 0);
+    match_number:int (id: 1);
+    set_number:int (id: 2);
+    comp_level:string (id: 3);
+    actions_list:[Action] (id:4);
+
+    // If this is for pre-scouting, then the server should accept this
+    // submission. I.e. checking that the match information exists in the match
+    // list should be skipped.
+    pre_scouting:bool (id: 5);
+}
\ No newline at end of file
diff --git a/scouting/webserver/requests/messages/submit_2024_actions_response.fbs b/scouting/webserver/requests/messages/submit_2024_actions_response.fbs
new file mode 100644
index 0000000..100172b
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_2024_actions_response.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table Submit2024ActionsResponse {
+    // empty response
+}
+
+root_type Submit2024ActionsResponse;
\ No newline at end of file
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 9628c0d..75b438b 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -14,9 +14,13 @@
 	"github.com/frc971/971-Robot-Code/scouting/db"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2024_data_scouting"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2024_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2024_data_scouting"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2024_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
@@ -31,6 +35,8 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_pit_images_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_2024_actions"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_2024_actions_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking"
@@ -53,6 +59,8 @@
 type RequestAllNotesResponseT = request_all_notes_response.RequestAllNotesResponseT
 type Request2023DataScouting = request_2023_data_scouting.Request2023DataScouting
 type Request2023DataScoutingResponseT = request_2023_data_scouting_response.Request2023DataScoutingResponseT
+type Request2024DataScouting = request_2024_data_scouting.Request2024DataScouting
+type Request2024DataScoutingResponseT = request_2024_data_scouting_response.Request2024DataScoutingResponseT
 type SubmitNotes = submit_notes.SubmitNotes
 type SubmitNotesResponseT = submit_notes_response.SubmitNotesResponseT
 type SubmitPitImage = submit_pit_image.SubmitPitImage
@@ -71,9 +79,14 @@
 type SubmitDriverRankingResponseT = submit_driver_ranking_response.SubmitDriverRankingResponseT
 type SubmitActions = submit_actions.SubmitActions
 type Action = submit_actions.Action
+type Action2024 = submit_2024_actions.Action
 type SubmitActionsResponseT = submit_actions_response.SubmitActionsResponseT
+type Submit2024Actions = submit_2024_actions.Submit2024Actions
+type Submit2024ActionsResponseT = submit_2024_actions_response.Submit2024ActionsResponseT
 type Delete2023DataScouting = delete_2023_data_scouting.Delete2023DataScouting
 type Delete2023DataScoutingResponseT = delete_2023_data_scouting_response.Delete2023DataScoutingResponseT
+type Delete2024DataScouting = delete_2024_data_scouting.Delete2024DataScouting
+type Delete2024DataScoutingResponseT = delete_2024_data_scouting_response.Delete2024DataScoutingResponseT
 
 // The interface we expect the database abstraction to conform to.
 // We use an interface here because it makes unit testing easier.
@@ -81,12 +94,15 @@
 	AddToMatch(db.TeamMatch) error
 	AddToShift(db.Shift) error
 	AddToStats2023(db.Stats2023) error
+	AddToStats2024(db.Stats2024) error
 	ReturnMatches() ([]db.TeamMatch, error)
 	ReturnAllNotes() ([]db.NotesData, error)
 	ReturnAllDriverRankings() ([]db.DriverRankingData, error)
 	ReturnAllShifts() ([]db.Shift, error)
 	ReturnStats2023() ([]db.Stats2023, error)
 	ReturnStats2023ForTeam(teamNumber string, matchNumber int32, setNumber int32, compLevel string, preScouting bool) ([]db.Stats2023, error)
+	ReturnStats2024() ([]db.Stats2024, error)
+	ReturnStats2024ForTeam(teamNumber string, matchNumber int32, setNumber int32, compLevel string, preScouting bool) ([]db.Stats2024, error)
 	QueryAllShifts(int) ([]db.Shift, error)
 	QueryNotes(string) ([]string, error)
 	QueryPitImages(string) ([]db.RequestedPitImage, error)
@@ -96,6 +112,7 @@
 	AddDriverRanking(db.DriverRankingData) error
 	AddAction(db.Action) error
 	DeleteFromStats(string, int32, int32, string) error
+	DeleteFromStats2024(string, int32, int32, string) error
 	DeleteFromActions(string, int32, int32, string) error
 }
 
@@ -182,6 +199,7 @@
 }
 
 func (handler requestAllMatchesHandler) teamHasBeenDataScouted(key MatchAssemblyKey, teamNumber string) (bool, error) {
+	// TODO change this to reference 2024 stats
 	stats, err := handler.db.ReturnStats2023ForTeam(
 		teamNumber, key.MatchNumber, key.SetNumber, key.CompLevel, false)
 	if err != nil {
@@ -426,6 +444,165 @@
 	w.Write(builder.FinishedBytes())
 }
 
+func ConvertActionsToStat2024(submit2024Actions *submit_2024_actions.Submit2024Actions) (db.Stats2024, error) {
+	overall_time := int64(0)
+	cycles := int64(0)
+	picked_up := false
+	lastPlacedTime := int64(0)
+	stat := db.Stats2024{
+		PreScouting: submit2024Actions.PreScouting(), TeamNumber: string(submit2024Actions.TeamNumber()), MatchNumber: submit2024Actions.MatchNumber(), SetNumber: submit2024Actions.SetNumber(), CompLevel: string(submit2024Actions.CompLevel()),
+		StartingQuadrant: 0, SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+		Speaker: 0, Amp: 0, SpeakerAmplified: 0, AmpAmplified: 0, NotesDropped: 0, Penalties: 0,
+		TrapNote: false, AvgCycle: 0, Park: false, OnStage: false, Harmony: false, CollectedBy: "",
+	}
+	// Loop over all actions.
+	for i := 0; i < submit2024Actions.ActionsListLength(); i++ {
+		var action submit_2024_actions.Action
+		if !submit2024Actions.ActionsList(&action, i) {
+			return db.Stats2024{}, errors.New(fmt.Sprintf("Failed to parse submit_2024_actions.Action"))
+		}
+		actionTable := new(flatbuffers.Table)
+		action_type := action.ActionTakenType()
+		if !action.ActionTaken(actionTable) {
+			return db.Stats2024{}, errors.New(fmt.Sprint("Failed to parse sub-action or sub-action was missing"))
+		}
+		if action_type == submit_2024_actions.ActionTypeStartMatchAction {
+			var startMatchAction submit_2024_actions.StartMatchAction
+			startMatchAction.Init(actionTable.Bytes, actionTable.Pos)
+			stat.StartingQuadrant = startMatchAction.Position()
+		} else if action_type == submit_2024_actions.ActionTypeMobilityAction {
+			var mobilityAction submit_2024_actions.MobilityAction
+			mobilityAction.Init(actionTable.Bytes, actionTable.Pos)
+			if mobilityAction.Mobility() {
+				stat.MobilityAuto = true
+			}
+
+		} else if action_type == submit_2024_actions.ActionTypePenaltyAction {
+			var penaltyAction submit_2024_actions.PenaltyAction
+			penaltyAction.Init(actionTable.Bytes, actionTable.Pos)
+			stat.Penalties += 1
+
+		} else if action_type == submit_2024_actions.ActionTypePickupNoteAction {
+			var pick_up_action submit_2024_actions.PickupNoteAction
+			pick_up_action.Init(actionTable.Bytes, actionTable.Pos)
+			if picked_up == true {
+				auto := pick_up_action.Auto()
+				if auto == false {
+					stat.NotesDropped += 1
+				} else {
+					stat.NotesDroppedAuto += 1
+				}
+			} else {
+				picked_up = true
+			}
+		} else if action_type == submit_2024_actions.ActionTypePlaceNoteAction {
+			var place_action submit_2024_actions.PlaceNoteAction
+			place_action.Init(actionTable.Bytes, actionTable.Pos)
+			if !picked_up {
+				return db.Stats2024{}, errors.New(fmt.Sprintf("Got PlaceNoteAction without corresponding PickupObjectAction"))
+			}
+			score_type := place_action.ScoreType()
+			auto := place_action.Auto()
+			if score_type == submit_2024_actions.ScoreTypekAMP && auto {
+				stat.AmpAuto += 1
+			} else if score_type == submit_2024_actions.ScoreTypekAMP && !auto {
+				stat.Amp += 1
+			} else if score_type == submit_2024_actions.ScoreTypekAMP_AMPLIFIED && !auto {
+				stat.AmpAmplified += 1
+			} else if score_type == submit_2024_actions.ScoreTypekSPEAKER && !auto {
+				stat.Speaker += 1
+			} else if score_type == submit_2024_actions.ScoreTypekSPEAKER && auto {
+				stat.SpeakerAuto += 1
+			} else if score_type == submit_2024_actions.ScoreTypekSPEAKER_AMPLIFIED && !auto {
+				stat.SpeakerAmplified += 1
+			} else {
+				return db.Stats2024{}, errors.New(fmt.Sprintf("Got unknown ObjectType/ScoreLevel/Auto combination"))
+			}
+			picked_up = false
+			if lastPlacedTime != int64(0) {
+				// If this is not the first time we place,
+				// start counting cycle time. We define cycle
+				// time as the time between placements.
+				overall_time += int64(action.Timestamp()) - lastPlacedTime
+				cycles += 1
+			}
+			lastPlacedTime = int64(action.Timestamp())
+		} else if action_type == submit_2024_actions.ActionTypeEndMatchAction {
+			var endMatchAction submit_2024_actions.EndMatchAction
+			endMatchAction.Init(actionTable.Bytes, actionTable.Pos)
+			if endMatchAction.StageType() == submit_2024_actions.StageTypekON_STAGE {
+				stat.OnStage = true
+			} else if endMatchAction.StageType() == submit_2024_actions.StageTypekPARK {
+				stat.Park = true
+			} else if endMatchAction.StageType() == submit_2024_actions.StageTypekHARMONY {
+				stat.Harmony = true
+			}
+			stat.TrapNote = endMatchAction.TrapNote()
+		}
+	}
+	if cycles != 0 {
+		stat.AvgCycle = overall_time / cycles
+	} else {
+		stat.AvgCycle = 0
+	}
+	return stat, nil
+}
+
+// Handles a Request2024DataScouting request.
+type request2024DataScoutingHandler struct {
+	db Database
+}
+
+func (handler request2024DataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	requestBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
+		return
+	}
+
+	_, success := parseRequest(w, requestBytes, "Request2024DataScouting", request_2024_data_scouting.GetRootAsRequest2024DataScouting)
+	if !success {
+		return
+	}
+
+	stats, err := handler.db.ReturnStats2024()
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
+		return
+	}
+
+	var response Request2024DataScoutingResponseT
+	for _, stat := range stats {
+		response.StatsList = append(response.StatsList, &request_2024_data_scouting_response.Stats2024T{
+			TeamNumber:       stat.TeamNumber,
+			MatchNumber:      stat.MatchNumber,
+			SetNumber:        stat.SetNumber,
+			CompLevel:        stat.CompLevel,
+			StartingQuadrant: stat.StartingQuadrant,
+			SpeakerAuto:      stat.SpeakerAuto,
+			AmpAuto:          stat.AmpAuto,
+			NotesDroppedAuto: stat.NotesDroppedAuto,
+			MobilityAuto:     stat.MobilityAuto,
+			Speaker:          stat.Speaker,
+			Amp:              stat.Amp,
+			SpeakerAmplified: stat.SpeakerAmplified,
+			AmpAmplified:     stat.AmpAmplified,
+			NotesDropped:     stat.NotesDropped,
+			Penalties:        stat.Penalties,
+			TrapNote:         stat.TrapNote,
+			AvgCycle:         stat.AvgCycle,
+			Park:             stat.Park,
+			OnStage:          stat.OnStage,
+			Harmony:          stat.Harmony,
+			CollectedBy:      stat.CollectedBy,
+		})
+	}
+
+	builder := flatbuffers.NewBuilder(50 * 1024)
+	builder.Finish((&response).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
 func ConvertActionsToStat(submitActions *submit_actions.SubmitActions) (db.Stats2023, error) {
 	overall_time := int64(0)
 	cycles := int64(0)
@@ -926,6 +1103,77 @@
 	w.Write(builder.FinishedBytes())
 }
 
+type submit2024ActionsHandler struct {
+	db Database
+}
+
+func (handler submit2024ActionsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	// Get the username of the person submitting the data.
+	username := parseUsername(req)
+
+	requestBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
+		return
+	}
+
+	request, success := parseRequest(w, requestBytes, "Submit2024Actions", submit_2024_actions.GetRootAsSubmit2024Actions)
+	if !success {
+		return
+	}
+
+	log.Println("Got actions for match", request.MatchNumber(), "team", request.TeamNumber(), "from", username)
+
+	for i := 0; i < request.ActionsListLength(); i++ {
+
+		var action Action2024
+		request.ActionsList(&action, i)
+
+		dbAction := db.Action{
+			PreScouting: request.PreScouting(),
+			TeamNumber:  string(request.TeamNumber()),
+			MatchNumber: request.MatchNumber(),
+			SetNumber:   request.SetNumber(),
+			CompLevel:   string(request.CompLevel()),
+			//TODO: Serialize CompletedAction
+			CompletedAction: []byte{},
+			Timestamp:       action.Timestamp(),
+			CollectedBy:     username,
+		}
+
+		// Do some error checking.
+		if action.Timestamp() < 0 {
+			respondWithError(w, http.StatusBadRequest, fmt.Sprint(
+				"Invalid timestamp field value of ", action.Timestamp()))
+			return
+		}
+
+		err = handler.db.AddAction(dbAction)
+		if err != nil {
+			respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to add action to database: ", err))
+			return
+		}
+	}
+
+	stats, err := ConvertActionsToStat2024(request)
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to convert actions to stats: ", err))
+		return
+	}
+
+	stats.CollectedBy = username
+
+	err = handler.db.AddToStats2024(stats)
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit stats: ", stats, ": ", err))
+		return
+	}
+
+	builder := flatbuffers.NewBuilder(50 * 1024)
+	builder.Finish((&SubmitActionsResponseT{}).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
 type submitActionsHandler struct {
 	db Database
 }
@@ -997,6 +1245,50 @@
 	w.Write(builder.FinishedBytes())
 }
 
+type Delete2024DataScoutingHandler struct {
+	db Database
+}
+
+func (handler Delete2024DataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	requestBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
+		return
+	}
+
+	request, success := parseRequest(w, requestBytes, "Delete2024DataScouting", delete_2024_data_scouting.GetRootAsDelete2024DataScouting)
+	if !success {
+		return
+	}
+
+	err = handler.db.DeleteFromStats2024(
+		string(request.CompLevel()),
+		request.MatchNumber(),
+		request.SetNumber(),
+		string(request.TeamNumber()))
+
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete from stats2024: %v", err))
+		return
+	}
+
+	err = handler.db.DeleteFromActions(
+		string(request.CompLevel()),
+		request.MatchNumber(),
+		request.SetNumber(),
+		string(request.TeamNumber()))
+
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete from actions: %v", err))
+		return
+	}
+
+	var response Delete2024DataScoutingResponseT
+	builder := flatbuffers.NewBuilder(10)
+	builder.Finish((&response).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
 type Delete2023DataScoutingHandler struct {
 	db Database
 }
@@ -1047,6 +1339,7 @@
 	scoutingServer.Handle("/requests/request/all_notes", requestAllNotesHandler{db})
 	scoutingServer.Handle("/requests/request/all_driver_rankings", requestAllDriverRankingsHandler{db})
 	scoutingServer.Handle("/requests/request/2023_data_scouting", request2023DataScoutingHandler{db})
+	scoutingServer.Handle("/requests/request/2024_data_scouting", request2024DataScoutingHandler{db})
 	scoutingServer.Handle("/requests/submit/submit_notes", submitNoteScoutingHandler{db})
 	scoutingServer.Handle("/requests/submit/submit_pit_image", submitPitImageScoutingHandler{db})
 	scoutingServer.Handle("/requests/request/pit_images", requestPitImagesHandler{db})
@@ -1056,5 +1349,7 @@
 	scoutingServer.Handle("/requests/request/shift_schedule", requestShiftScheduleHandler{db})
 	scoutingServer.Handle("/requests/submit/submit_driver_ranking", SubmitDriverRankingHandler{db})
 	scoutingServer.Handle("/requests/submit/submit_actions", submitActionsHandler{db})
+	scoutingServer.Handle("/requests/submit/submit_2024_actions", submit2024ActionsHandler{db})
 	scoutingServer.Handle("/requests/delete/delete_2023_data_scouting", Delete2023DataScoutingHandler{db})
+	scoutingServer.Handle("/requests/delete/delete_2024_data_scouting", Delete2024DataScoutingHandler{db})
 }
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index d81b659..67244d3 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -8,8 +8,11 @@
 	"github.com/frc971/971-Robot-Code/scouting/db"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/debug"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2024_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2024_data_scouting"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2024_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
@@ -23,6 +26,7 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_pit_images_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_2024_actions"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes"
@@ -209,6 +213,71 @@
 
 }
 
+// Validates that we can request the 2024 stats.
+func TestRequest2024DataScouting(t *testing.T) {
+	db := MockDatabase{
+		stats2024: []db.Stats2024{
+			{
+				PreScouting: false, TeamNumber: "342",
+				MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 4,
+				SpeakerAuto: 1, AmpAuto: 1, NotesDroppedAuto: 0, MobilityAuto: true,
+				Speaker: 4, Amp: 2, SpeakerAmplified: 1, AmpAmplified: 0,
+				NotesDropped: 2, Penalties: 2, TrapNote: true, AvgCycle: 0,
+				Park: true, OnStage: false, Harmony: false, CollectedBy: "alex",
+			},
+			{
+				PreScouting: false, TeamNumber: "982",
+				MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 2,
+				SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+				Speaker: 0, Amp: 2, SpeakerAmplified: 3, AmpAmplified: 2,
+				NotesDropped: 1, Penalties: 0, TrapNote: false, AvgCycle: 0,
+				Park: false, OnStage: true, Harmony: false, CollectedBy: "george",
+			},
+		},
+	}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&db, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&request_2024_data_scouting.Request2024DataScoutingT{}).Pack(builder))
+
+	response, err := debug.Request2024DataScouting("http://localhost:8080", builder.FinishedBytes())
+	if err != nil {
+		t.Fatal("Failed to request all matches: ", err)
+	}
+
+	expected := request_2024_data_scouting_response.Request2024DataScoutingResponseT{
+		StatsList: []*request_2024_data_scouting_response.Stats2024T{
+			{
+				PreScouting: false, TeamNumber: "342",
+				MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 4,
+				SpeakerAuto: 1, AmpAuto: 1, NotesDroppedAuto: 0, MobilityAuto: true,
+				Speaker: 4, Amp: 2, SpeakerAmplified: 1, AmpAmplified: 0,
+				NotesDropped: 2, Penalties: 2, TrapNote: true, AvgCycle: 0,
+				Park: true, OnStage: false, Harmony: false, CollectedBy: "alex",
+			},
+			{
+				PreScouting: false, TeamNumber: "982",
+				MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 2,
+				SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+				Speaker: 0, Amp: 2, SpeakerAmplified: 3, AmpAmplified: 2,
+				NotesDropped: 1, Penalties: 0, TrapNote: false, AvgCycle: 0,
+				Park: false, OnStage: true, Harmony: false, CollectedBy: "george",
+			},
+		},
+	}
+	if len(expected.StatsList) != len(response.StatsList) {
+		t.Fatal("Expected ", expected, ", but got ", *response)
+	}
+	for i, match := range expected.StatsList {
+		if !reflect.DeepEqual(*match, *response.StatsList[i]) {
+			t.Fatal("Expected for stats", i, ":", *match, ", but got:", *response.StatsList[i])
+		}
+	}
+}
+
 // Validates that we can request the 2023 stats.
 func TestRequest2023DataScouting(t *testing.T) {
 	db := MockDatabase{
@@ -290,6 +359,141 @@
 	}
 }
 
+// Validates that we can request the 2024 stats.
+func TestConvertActionsToStat2024(t *testing.T) {
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&submit_2024_actions.Submit2024ActionsT{
+		TeamNumber:  "4244",
+		MatchNumber: 3,
+		SetNumber:   1,
+		CompLevel:   "quals",
+		ActionsList: []*submit_2024_actions.ActionT{
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypeStartMatchAction,
+					Value: &submit_2024_actions.StartMatchActionT{
+						Position: 2,
+					},
+				},
+				Timestamp: 0,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypePickupNoteAction,
+					Value: &submit_2024_actions.PickupNoteActionT{
+						Auto: true,
+					},
+				},
+				Timestamp: 400,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypePickupNoteAction,
+					Value: &submit_2024_actions.PickupNoteActionT{
+						Auto: true,
+					},
+				},
+				Timestamp: 800,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypePlaceNoteAction,
+					Value: &submit_2024_actions.PlaceNoteActionT{
+						ScoreType: submit_2024_actions.ScoreTypekAMP,
+						Auto:      true,
+					},
+				},
+				Timestamp: 2000,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypeMobilityAction,
+					Value: &submit_2024_actions.MobilityActionT{
+						Mobility: true,
+					},
+				},
+				Timestamp: 2200,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type:  submit_2024_actions.ActionTypePenaltyAction,
+					Value: &submit_2024_actions.PenaltyActionT{},
+				},
+				Timestamp: 2400,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypePickupNoteAction,
+					Value: &submit_2024_actions.PickupNoteActionT{
+						Auto: false,
+					},
+				},
+				Timestamp: 2800,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypePlaceNoteAction,
+					Value: &submit_2024_actions.PlaceNoteActionT{
+						ScoreType: submit_2024_actions.ScoreTypekAMP_AMPLIFIED,
+						Auto:      false,
+					},
+				},
+				Timestamp: 3100,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypePickupNoteAction,
+					Value: &submit_2024_actions.PickupNoteActionT{
+						Auto: false,
+					},
+				},
+				Timestamp: 3500,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypePlaceNoteAction,
+					Value: &submit_2024_actions.PlaceNoteActionT{
+						ScoreType: submit_2024_actions.ScoreTypekSPEAKER_AMPLIFIED,
+						Auto:      false,
+					},
+				},
+				Timestamp: 3900,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypeEndMatchAction,
+					Value: &submit_2024_actions.EndMatchActionT{
+						StageType: submit_2024_actions.StageTypekHARMONY,
+						TrapNote:  false,
+					},
+				},
+				Timestamp: 4200,
+			},
+		},
+		PreScouting: false,
+	}).Pack(builder))
+
+	submit2024Actions := submit_2024_actions.GetRootAsSubmit2024Actions(builder.FinishedBytes(), 0)
+	response, err := ConvertActionsToStat2024(submit2024Actions)
+
+	if err != nil {
+		t.Fatal("Failed to convert actions to stats: ", err)
+	}
+
+	expected := db.Stats2024{
+		PreScouting: false, TeamNumber: "4244",
+		MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 2,
+		SpeakerAuto: 0, AmpAuto: 1, NotesDroppedAuto: 1, MobilityAuto: true,
+		Speaker: 0, Amp: 0, SpeakerAmplified: 1, AmpAmplified: 1,
+		NotesDropped: 0, Penalties: 1, TrapNote: false, AvgCycle: 950,
+		Park: false, OnStage: false, Harmony: true, CollectedBy: "",
+	}
+
+	if expected != response {
+		t.Fatal("Expected ", expected, ", but got ", response)
+	}
+}
+
 // Validates that we can request the 2023 stats.
 func TestConvertActionsToStat(t *testing.T) {
 	builder := flatbuffers.NewBuilder(1024)
@@ -931,6 +1135,90 @@
 	return (builder.FinishedBytes())
 }
 
+func TestAddingActions2024(t *testing.T) {
+	database := MockDatabase{}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&database, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&submit_2024_actions.Submit2024ActionsT{
+		TeamNumber:  "3421",
+		MatchNumber: 2,
+		SetNumber:   1,
+		CompLevel:   "quals",
+		ActionsList: []*submit_2024_actions.ActionT{
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypePickupNoteAction,
+					Value: &submit_2024_actions.PickupNoteActionT{
+						Auto: true,
+					},
+				},
+				Timestamp: 1800,
+			},
+			{
+				ActionTaken: &submit_2024_actions.ActionTypeT{
+					Type: submit_2024_actions.ActionTypePlaceNoteAction,
+					Value: &submit_2024_actions.PlaceNoteActionT{
+						ScoreType: submit_2024_actions.ScoreTypekSPEAKER,
+						Auto:      false,
+					},
+				},
+				Timestamp: 2500,
+			},
+		},
+		PreScouting: true,
+	}).Pack(builder))
+
+	_, err := debug.Submit2024Actions("http://localhost:8080", builder.FinishedBytes())
+	if err != nil {
+		t.Fatal("Failed to submit actions: ", err)
+	}
+
+	expectedActions := []db.Action{
+		{
+			PreScouting:     true,
+			TeamNumber:      "3421",
+			MatchNumber:     2,
+			SetNumber:       1,
+			CompLevel:       "quals",
+			CollectedBy:     "debug_cli",
+			CompletedAction: []byte{},
+			Timestamp:       1800,
+		},
+		{
+			PreScouting:     true,
+			TeamNumber:      "3421",
+			MatchNumber:     2,
+			SetNumber:       1,
+			CompLevel:       "quals",
+			CollectedBy:     "debug_cli",
+			CompletedAction: []byte{},
+			Timestamp:       2500,
+		},
+	}
+
+	expectedStats := []db.Stats2024{
+		db.Stats2024{
+			PreScouting: true, TeamNumber: "3421",
+			MatchNumber: 2, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 0,
+			SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+			Speaker: 1, Amp: 0, SpeakerAmplified: 0, AmpAmplified: 0,
+			NotesDropped: 0, Penalties: 0, TrapNote: false, AvgCycle: 0,
+			Park: false, OnStage: false, Harmony: false, CollectedBy: "debug_cli",
+		},
+	}
+
+	if !reflect.DeepEqual(expectedActions, database.actions) {
+		t.Fatal("Expected ", expectedActions, ", but got:", database.actions)
+	}
+	if !reflect.DeepEqual(expectedStats, database.stats2024) {
+		t.Fatal("Expected ", expectedStats, ", but got:", database.stats2024)
+	}
+}
+
 func TestAddingActions(t *testing.T) {
 	database := MockDatabase{}
 	scoutingServer := server.NewScoutingServer()
@@ -1155,6 +1443,100 @@
 	}
 }
 
+// Validates that we can delete 2024 stats.
+func TestDeleteFromStats2024(t *testing.T) {
+	database := MockDatabase{
+		stats2024: []db.Stats2024{
+			{
+				PreScouting: false, TeamNumber: "746",
+				MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 2,
+				SpeakerAuto: 0, AmpAuto: 1, NotesDroppedAuto: 1, MobilityAuto: true,
+				Speaker: 0, Amp: 1, SpeakerAmplified: 1, AmpAmplified: 1,
+				NotesDropped: 0, Penalties: 1, TrapNote: true, AvgCycle: 233,
+				Park: false, OnStage: false, Harmony: true, CollectedBy: "alek",
+			},
+			{
+				PreScouting: false, TeamNumber: "244",
+				MatchNumber: 5, SetNumber: 3, CompLevel: "quals", StartingQuadrant: 1,
+				SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+				Speaker: 0, Amp: 0, SpeakerAmplified: 3, AmpAmplified: 1,
+				NotesDropped: 0, Penalties: 1, TrapNote: false, AvgCycle: 120,
+				Park: false, OnStage: true, Harmony: false, CollectedBy: "kacey",
+			},
+		},
+		actions: []db.Action{
+			{
+				PreScouting:     true,
+				TeamNumber:      "746",
+				MatchNumber:     3,
+				SetNumber:       1,
+				CompLevel:       "quals",
+				CollectedBy:     "debug_cli",
+				CompletedAction: []byte{},
+				Timestamp:       2400,
+			},
+			{
+				PreScouting:     true,
+				TeamNumber:      "244",
+				MatchNumber:     5,
+				SetNumber:       3,
+				CompLevel:       "quals",
+				CollectedBy:     "debug_cli",
+				CompletedAction: []byte{},
+				Timestamp:       1009,
+			},
+		},
+	}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&database, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&delete_2024_data_scouting.Delete2024DataScoutingT{
+		CompLevel:   "quals",
+		MatchNumber: 3,
+		SetNumber:   1,
+		TeamNumber:  "746",
+	}).Pack(builder))
+
+	_, err := debug.Delete2024DataScouting("http://localhost:8080", builder.FinishedBytes())
+	if err != nil {
+		t.Fatal("Failed to delete from data scouting 2024", err)
+	}
+
+	expectedActions := []db.Action{
+		{
+			PreScouting:     true,
+			TeamNumber:      "244",
+			MatchNumber:     5,
+			SetNumber:       3,
+			CompLevel:       "quals",
+			CollectedBy:     "debug_cli",
+			CompletedAction: []byte{},
+			Timestamp:       1009,
+		},
+	}
+
+	expectedStats := []db.Stats2024{
+		{
+			PreScouting: false, TeamNumber: "244",
+			MatchNumber: 5, SetNumber: 3, CompLevel: "quals", StartingQuadrant: 1,
+			SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
+			Speaker: 0, Amp: 0, SpeakerAmplified: 3, AmpAmplified: 1,
+			NotesDropped: 0, Penalties: 1, TrapNote: false, AvgCycle: 120,
+			Park: false, OnStage: true, Harmony: false, CollectedBy: "kacey",
+		},
+	}
+
+	if !reflect.DeepEqual(expectedActions, database.actions) {
+		t.Fatal("Expected ", expectedActions, ", but got:", database.actions)
+	}
+	if !reflect.DeepEqual(expectedStats, database.stats2024) {
+		t.Fatal("Expected ", expectedStats, ", but got:", database.stats2024)
+	}
+}
+
 // A mocked database we can use for testing. Add functionality to this as
 // needed for your tests.
 
@@ -1164,6 +1546,7 @@
 	shiftSchedule  []db.Shift
 	driver_ranking []db.DriverRankingData
 	stats2023      []db.Stats2023
+	stats2024      []db.Stats2024
 	actions        []db.Action
 	images         []db.PitImage
 }
@@ -1177,6 +1560,11 @@
 	database.stats2023 = append(database.stats2023, stats2023)
 	return nil
 }
+
+func (database *MockDatabase) AddToStats2024(stats2024 db.Stats2024) error {
+	database.stats2024 = append(database.stats2024, stats2024)
+	return nil
+}
 func (database *MockDatabase) ReturnMatches() ([]db.TeamMatch, error) {
 	return database.matches, nil
 }
@@ -1185,6 +1573,10 @@
 	return database.stats2023, nil
 }
 
+func (database *MockDatabase) ReturnStats2024() ([]db.Stats2024, error) {
+	return database.stats2024, nil
+}
+
 func (database *MockDatabase) ReturnStats2023ForTeam(teamNumber string, matchNumber int32, setNumber int32, compLevel string, preScouting bool) ([]db.Stats2023, error) {
 	var results []db.Stats2023
 	for _, stats := range database.stats2023 {
@@ -1195,6 +1587,16 @@
 	return results, nil
 }
 
+func (database *MockDatabase) ReturnStats2024ForTeam(teamNumber string, matchNumber int32, setNumber int32, compLevel string, preScouting bool) ([]db.Stats2024, error) {
+	var results []db.Stats2024
+	for _, stats := range database.stats2024 {
+		if stats.TeamNumber == teamNumber && stats.MatchNumber == matchNumber && stats.SetNumber == setNumber && stats.CompLevel == compLevel && stats.PreScouting == preScouting {
+			results = append(results, stats)
+		}
+	}
+	return results, nil
+}
+
 func (database *MockDatabase) QueryNotes(requestedTeam string) ([]string, error) {
 	var results []string
 	for _, data := range database.notes {
@@ -1281,6 +1683,19 @@
 	return nil
 }
 
+func (database *MockDatabase) DeleteFromStats2024(compLevel_ string, matchNumber_ int32, setNumber_ int32, teamNumber_ string) error {
+	for i, stat := range database.stats2024 {
+		if stat.CompLevel == compLevel_ &&
+			stat.MatchNumber == matchNumber_ &&
+			stat.SetNumber == setNumber_ &&
+			stat.TeamNumber == teamNumber_ {
+			// Match found, remove the element from the array.
+			database.stats2024 = append(database.stats2024[:i], database.stats2024[i+1:]...)
+		}
+	}
+	return nil
+}
+
 func (database *MockDatabase) DeleteFromActions(compLevel_ string, matchNumber_ int32, setNumber_ int32, teamNumber_ string) error {
 	for i, action := range database.actions {
 		if action.CompLevel == compLevel_ &&
diff --git a/y2018/control_loops/superstructure/arm/trajectory_plot.cc b/y2018/control_loops/superstructure/arm/trajectory_plot.cc
index 29dc085..eeb0fbc 100644
--- a/y2018/control_loops/superstructure/arm/trajectory_plot.cc
+++ b/y2018/control_loops/superstructure/arm/trajectory_plot.cc
@@ -255,7 +255,7 @@
   }
 
   if (FLAGS_plot) {
-    frc971::analysis::Plotter plotter;
+    aos::analysis::Plotter plotter;
 
     plotter.AddFigure();
     plotter.Title("Trajectory");
diff --git a/y2023/control_loops/superstructure/arm/arm_design.cc b/y2023/control_loops/superstructure/arm/arm_design.cc
index d17535a..8c5664e 100644
--- a/y2023/control_loops/superstructure/arm/arm_design.cc
+++ b/y2023/control_loops/superstructure/arm/arm_design.cc
@@ -20,7 +20,7 @@
 namespace y2023::control_loops::superstructure::arm {
 
 int Main() {
-  frc971::analysis::Plotter plotter;
+  aos::analysis::Plotter plotter;
 
   frc971::control_loops::arm::Dynamics dynamics(kArmConstants);
 
diff --git a/y2023/control_loops/superstructure/arm/trajectory_plot.cc b/y2023/control_loops/superstructure/arm/trajectory_plot.cc
index 9b99660..8488d72 100644
--- a/y2023/control_loops/superstructure/arm/trajectory_plot.cc
+++ b/y2023/control_loops/superstructure/arm/trajectory_plot.cc
@@ -396,7 +396,7 @@
   }
 
   if (FLAGS_plot) {
-    frc971::analysis::Plotter plotter;
+    aos::analysis::Plotter plotter;
 
     plotter.AddFigure();
     plotter.Title("Input spline");
diff --git a/y2024/BUILD b/y2024/BUILD
index 39a023a..875bf84 100644
--- a/y2024/BUILD
+++ b/y2024/BUILD
@@ -322,6 +322,7 @@
     data = [
         ":aos_config",
         "//aos/network:log_web_proxy_main",
+        "//y2024/www:field_main_bundle.min.js",
         "//y2024/www:files",
     ],
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/y2024/constants/common.jinja2 b/y2024/constants/common.jinja2
index 21b061c..d6db5e5 100644
--- a/y2024/constants/common.jinja2
+++ b/y2024/constants/common.jinja2
@@ -1,7 +1,7 @@
 {% set pi = 3.14159265 %}
 
 {# we do this here so we keep the encoder ratio in plaintext and also keep the math we're using. #}
-{% set intake_pivot_encoder_ratio = (16.0 / 64.0) * (18.0 / 62.0) %}
+{% set intake_pivot_encoder_ratio = (24.0 / 15.0) %}
 
 {%set zeroing_sample_size = 200 %}
 
diff --git a/y2024/control_loops/python/intake_pivot.py b/y2024/control_loops/python/intake_pivot.py
index 9e7543b..f32aa8b 100644
--- a/y2024/control_loops/python/intake_pivot.py
+++ b/y2024/control_loops/python/intake_pivot.py
@@ -20,16 +20,15 @@
 kIntakePivot = angular_system.AngularSystemParams(
     name='IntakePivot',
     motor=control_loop.KrakenFOC(),
-    # TODO(Niko): Change gear ratios when we have all of them
-    G=0.02,
-    J=0.34,
+    G=(16.0 / 60.0) * (18.0 / 62.0) * (18.0 / 62.0) * (15.0 / 24.0),
+    J=0.34,  # Borrowed from 2022, 0.035 seems too low
     q_pos=0.40,
     q_vel=20.0,
     kalman_q_pos=0.12,
     kalman_q_vel=2.0,
     kalman_q_voltage=4.0,
     kalman_r_position=0.05,
-    radius=13 * 0.0254)
+    radius=6.85 * 0.0254)
 
 
 def main(argv):
diff --git a/y2024/www/BUILD b/y2024/www/BUILD
index 726a354..5ff91d6 100644
--- a/y2024/www/BUILD
+++ b/y2024/www/BUILD
@@ -11,12 +11,11 @@
     visibility = ["//visibility:public"],
 )
 
-#Need to add 2024 field png
 genrule(
     name = "2024_field_png",
-    srcs = ["//third_party/y2023/field:pictures"],
+    srcs = ["//third_party/y2024/field:pictures"],
     outs = ["2024.png"],
-    cmd = "cp third_party/y2023/field/2023.png $@",
+    cmd = "cp third_party/y2024/field/2024.png $@",
 )
 
 ts_project(
diff --git a/y2024/www/field_handler.ts b/y2024/www/field_handler.ts
index 6c5f2e3..f383c08 100644
--- a/y2024/www/field_handler.ts
+++ b/y2024/www/field_handler.ts
@@ -16,10 +16,56 @@
 const FIELD_SIDE_Y = FIELD_WIDTH / 2;
 const FIELD_EDGE_X = FIELD_LENGTH / 2;
 
-const ROBOT_WIDTH = 25 * IN_TO_M;
+const ROBOT_WIDTH = 29 * IN_TO_M;
 const ROBOT_LENGTH = 32 * IN_TO_M;
 
 export class FieldHandler {
+  private canvas = document.createElement('canvas');
+  private fieldImage: HTMLImageElement = new Image();
   constructor(private readonly connection: Connection) {
+    (document.getElementById('field') as HTMLElement).appendChild(this.canvas);
+
+    this.fieldImage.src = '2024.png';
+  }
+
+  drawField(): void {
+    const ctx = this.canvas.getContext('2d');
+    ctx.save();
+    ctx.scale(1.0, -1.0);
+    ctx.drawImage(
+        this.fieldImage, 0, 0, this.fieldImage.width, this.fieldImage.height,
+        -FIELD_EDGE_X, -FIELD_SIDE_Y, FIELD_LENGTH, FIELD_WIDTH);
+    ctx.restore();
+  }
+
+  draw(): void {
+    this.reset();
+    this.drawField();
+
+    window.requestAnimationFrame(() => this.draw());
+  }
+
+  reset(): void {
+    const ctx = this.canvas.getContext('2d');
+    // Empty space from the canvas boundary to the image
+    const IMAGE_PADDING = 10;
+    ctx.setTransform(1, 0, 0, 1, 0, 0);
+    const size = window.innerHeight * 0.9;
+    ctx.canvas.height = size;
+    const width = size / 2 + 20;
+    ctx.canvas.width = width;
+    ctx.clearRect(0, 0, size, width);
+
+    // Translate to center of display.
+    ctx.translate(width / 2, size / 2);
+    // Coordinate system is:
+    // x -> forward.
+    // y -> to the left.
+    ctx.rotate(-Math.PI / 2);
+    ctx.scale(1, -1);
+
+    const M_TO_PX = (size - IMAGE_PADDING) / FIELD_LENGTH;
+    ctx.scale(M_TO_PX, M_TO_PX);
+    ctx.lineWidth = 1 / M_TO_PX;
   }
 }
diff --git a/y2024/www/field_main.ts b/y2024/www/field_main.ts
index 24bc23a..ef8b99d 100644
--- a/y2024/www/field_main.ts
+++ b/y2024/www/field_main.ts
@@ -8,4 +8,4 @@
 
 const fieldHandler = new FieldHandler(conn);
 
-
+fieldHandler.draw();