Merge "Forward JoystickState to vision pis"
diff --git a/aos/starter/starterd_lib.cc b/aos/starter/starterd_lib.cc
index 485d1f1..b8b7343 100644
--- a/aos/starter/starterd_lib.cc
+++ b/aos/starter/starterd_lib.cc
@@ -84,7 +84,8 @@
     if (aos::configuration::MultiNode(config_msg_)) {
       std::string_view current_node = event_loop_.node()->name()->string_view();
       for (const aos::Application *application : *applications) {
-        CHECK(application->has_nodes());
+        CHECK(application->has_nodes())
+            << ": Missing nodes on " << aos::FlatbufferToJson(application);
         for (const flatbuffers::String *node : *application->nodes()) {
           if (node->string_view() == current_node) {
             AddApplication(application);
diff --git a/frc971/vision/BUILD b/frc971/vision/BUILD
index 936a441..f3ebecb 100644
--- a/frc971/vision/BUILD
+++ b/frc971/vision/BUILD
@@ -32,6 +32,13 @@
     visibility = ["//visibility:public"],
 )
 
+flatbuffer_ts_library(
+    name = "target_map_ts_fbs",
+    srcs = ["target_map.fbs"],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+)
+
 flatbuffer_py_library(
     name = "calibration_fbs_python",
     srcs = [
diff --git a/frc971/vision/target_map.fbs b/frc971/vision/target_map.fbs
index e635760..de79744 100644
--- a/frc971/vision/target_map.fbs
+++ b/frc971/vision/target_map.fbs
@@ -63,6 +63,13 @@
   // End-of-frame timestamp for the frame with tag detections.
   // (for use case 2.).
   monotonic_timestamp_ns:int64 (id: 2);
+
+  // Number of april tags rejected (cumulative) because
+  // of low decision margin (affected by lighting).
+  // We do the decision margin rejection in aprilrobotics
+  // so we don't have an excessive amount of random target
+  // detections (for use case 2).
+  rejections:uint64 (id: 3);
 }
 
 root_type TargetMap;
diff --git a/scouting/DriverRank/src/DriverRank.jl b/scouting/DriverRank/src/DriverRank.jl
index 39ac95e..e759fea 100755
--- a/scouting/DriverRank/src/DriverRank.jl
+++ b/scouting/DriverRank/src/DriverRank.jl
@@ -106,7 +106,8 @@
     input_csv::String,
     output_csv::String,
 )
-    df = DataFrame(CSV.File(input_csv))
+    # Force all team numbers to be parsed as strings.
+    df = DataFrame(CSV.File(input_csv, types=String))
 
     rank1 = "Rank 1 (best)"
     rank2 = "Rank 2"
diff --git a/scouting/db/db.go b/scouting/db/db.go
index adf1eae..8c7a93a 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -7,6 +7,7 @@
 	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
 	"gorm.io/gorm/logger"
+	"strconv"
 )
 
 type Database struct {
@@ -19,7 +20,7 @@
 	CompLevel        string `gorm:"primaryKey"`
 	Alliance         string `gorm:"primaryKey"` // "R" or "B"
 	AlliancePosition int32  `gorm:"primaryKey"` // 1, 2, or 3
-	TeamNumber       int32
+	TeamNumber       string
 }
 
 type Shift struct {
@@ -75,7 +76,7 @@
 	LowConesAuto, MiddleConesAuto, HighConesAuto, ConesDroppedAuto int32
 	LowCubes, MiddleCubes, HighCubes, CubesDropped                 int32
 	LowCones, MiddleCones, HighCones, ConesDropped                 int32
-	AvgCycle                                                       int32
+	AvgCycle                                                       int64
 	// The username of the person who collected these statistics.
 	// "unknown" if submitted without logging in.
 	// Empty if the stats have not yet been collected.
@@ -195,7 +196,7 @@
 }
 
 func (database *Database) AddToStats(s Stats) error {
-	matches, err := database.queryMatches(s.TeamNumber)
+	matches, err := database.queryMatches(strconv.Itoa(int(s.TeamNumber)))
 	if err != nil {
 		return err
 	}
@@ -344,7 +345,7 @@
 	return rankins, result.Error
 }
 
-func (database *Database) queryMatches(teamNumber_ int32) ([]TeamMatch, error) {
+func (database *Database) queryMatches(teamNumber_ string) ([]TeamMatch, error) {
 	var matches []TeamMatch
 	result := database.
 		Where("team_number = $1", teamNumber_).
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index a6bfa79..294dc13 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -6,7 +6,6 @@
 	"os"
 	"os/exec"
 	"reflect"
-	"strconv"
 	"strings"
 	"testing"
 	"time"
@@ -75,27 +74,27 @@
 	correct := []TeamMatch{
 		TeamMatch{
 			MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 9999,
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "9999",
 		},
 		TeamMatch{
 			MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 2, TeamNumber: 1000,
+			Alliance: "R", AlliancePosition: 2, TeamNumber: "1000",
 		},
 		TeamMatch{
 			MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 3, TeamNumber: 777,
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "777",
 		},
 		TeamMatch{
 			MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 1, TeamNumber: 0000,
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "0000",
 		},
 		TeamMatch{
 			MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 2, TeamNumber: 4321,
+			Alliance: "B", AlliancePosition: 2, TeamNumber: "4321",
 		},
 		TeamMatch{
 			MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 3, TeamNumber: 1234,
+			Alliance: "B", AlliancePosition: 3, TeamNumber: "1234",
 		},
 	}
 
@@ -202,17 +201,17 @@
 	}
 	matches := []TeamMatch{
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 1236},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "1236"},
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 2, TeamNumber: 1001},
+			Alliance: "R", AlliancePosition: 2, TeamNumber: "1001"},
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 3, TeamNumber: 777},
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "777"},
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 1, TeamNumber: 1000},
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "1000"},
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 2, TeamNumber: 4321},
+			Alliance: "B", AlliancePosition: 2, TeamNumber: "4321"},
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 3, TeamNumber: 1234},
+			Alliance: "B", AlliancePosition: 3, TeamNumber: "1234"},
 	}
 
 	for _, match := range matches {
@@ -292,15 +291,15 @@
 
 	matches := []TeamMatch{
 		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 6344},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "6344"},
 		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 2, TeamNumber: 7454},
+			Alliance: "R", AlliancePosition: 2, TeamNumber: "7454"},
 		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 3, TeamNumber: 4354},
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "4354"},
 		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 1, TeamNumber: 6533},
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "6533"},
 		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 2, TeamNumber: 8354},
+			Alliance: "B", AlliancePosition: 2, TeamNumber: "8354"},
 	}
 
 	for _, match := range matches {
@@ -360,11 +359,11 @@
 
 	matches := []TeamMatch{
 		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "qm",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 6344},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "6344"},
 		TeamMatch{MatchNumber: 4, SetNumber: 1, CompLevel: "qm",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 7454},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "7454"},
 		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "qm",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 6344},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "6344"},
 	}
 
 	for _, match := range matches {
@@ -413,17 +412,17 @@
 
 	matches := []TeamMatch{
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 1236},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "1236"},
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 2, TeamNumber: 1001},
+			Alliance: "R", AlliancePosition: 2, TeamNumber: "1001"},
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 3, TeamNumber: 777},
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "777"},
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 1, TeamNumber: 1000},
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "1000"},
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 2, TeamNumber: 4321},
+			Alliance: "B", AlliancePosition: 2, TeamNumber: "4321"},
 		TeamMatch{MatchNumber: 7, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 3, TeamNumber: 1234},
+			Alliance: "B", AlliancePosition: 3, TeamNumber: "1234"},
 	}
 
 	for _, match := range matches {
@@ -527,25 +526,25 @@
 
 	originalMatches := []TeamMatch{
 		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 1111},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "1111"},
 		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 1, TeamNumber: 2314},
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "2314"},
 		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 3, TeamNumber: 1742},
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "1742"},
 		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 2, TeamNumber: 2454},
+			Alliance: "B", AlliancePosition: 2, TeamNumber: "2454"},
 		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 3, TeamNumber: 3242},
+			Alliance: "B", AlliancePosition: 3, TeamNumber: "3242"},
 	}
 
 	// Matches for which we want to delete the stats.
 	matches := []TeamMatch{
 		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
-			TeamNumber: 1111},
+			TeamNumber: "1111"},
 		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
-			TeamNumber: 2314},
+			TeamNumber: "2314"},
 		TeamMatch{MatchNumber: 5, SetNumber: 1, CompLevel: "quals",
-			TeamNumber: 1742},
+			TeamNumber: "1742"},
 	}
 
 	for _, match := range originalMatches {
@@ -560,7 +559,7 @@
 	}
 
 	for _, match := range matches {
-		err := fixture.db.DeleteFromStats(match.CompLevel, match.MatchNumber, match.SetNumber, strconv.Itoa(int(match.TeamNumber)))
+		err := fixture.db.DeleteFromStats(match.CompLevel, match.MatchNumber, match.SetNumber, match.TeamNumber)
 		check(t, err, "Failed to delete stat")
 	}
 
@@ -663,17 +662,17 @@
 
 	matches := []TeamMatch{
 		TeamMatch{MatchNumber: 94, SetNumber: 2, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 1235},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "1235"},
 		TeamMatch{MatchNumber: 94, SetNumber: 2, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 2, TeamNumber: 1234},
+			Alliance: "R", AlliancePosition: 2, TeamNumber: "1234"},
 		TeamMatch{MatchNumber: 94, SetNumber: 2, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 3, TeamNumber: 1233},
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "1233"},
 		TeamMatch{MatchNumber: 94, SetNumber: 2, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 1, TeamNumber: 1232},
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "1232"},
 		TeamMatch{MatchNumber: 94, SetNumber: 2, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 2, TeamNumber: 1231},
+			Alliance: "B", AlliancePosition: 2, TeamNumber: "1231"},
 		TeamMatch{MatchNumber: 94, SetNumber: 2, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 3, TeamNumber: 1239},
+			Alliance: "B", AlliancePosition: 3, TeamNumber: "1239"},
 	}
 
 	for _, match := range matches {
@@ -759,15 +758,15 @@
 
 	correct := []TeamMatch{
 		TeamMatch{
-			MatchNumber: 8, SetNumber: 1, CompLevel: "quals", Alliance: "R", AlliancePosition: 1, TeamNumber: 6835},
+			MatchNumber: 8, SetNumber: 1, CompLevel: "quals", Alliance: "R", AlliancePosition: 1, TeamNumber: "6835"},
 		TeamMatch{
-			MatchNumber: 8, SetNumber: 1, CompLevel: "quals", Alliance: "R", AlliancePosition: 2, TeamNumber: 4834},
+			MatchNumber: 8, SetNumber: 1, CompLevel: "quals", Alliance: "R", AlliancePosition: 2, TeamNumber: "4834"},
 		TeamMatch{
-			MatchNumber: 9, SetNumber: 1, CompLevel: "quals", Alliance: "B", AlliancePosition: 3, TeamNumber: 9824},
+			MatchNumber: 9, SetNumber: 1, CompLevel: "quals", Alliance: "B", AlliancePosition: 3, TeamNumber: "9824"},
 		TeamMatch{
-			MatchNumber: 7, SetNumber: 2, CompLevel: "quals", Alliance: "B", AlliancePosition: 1, TeamNumber: 3732},
+			MatchNumber: 7, SetNumber: 2, CompLevel: "quals", Alliance: "B", AlliancePosition: 1, TeamNumber: "3732"},
 		TeamMatch{
-			MatchNumber: 8, SetNumber: 1, CompLevel: "quals", Alliance: "B", AlliancePosition: 1, TeamNumber: 3732},
+			MatchNumber: 8, SetNumber: 1, CompLevel: "quals", Alliance: "B", AlliancePosition: 1, TeamNumber: "3732"},
 	}
 
 	for i := 0; i < len(correct); i++ {
@@ -789,11 +788,11 @@
 
 	testDatabase := []TeamMatch{
 		TeamMatch{
-			MatchNumber: 9, SetNumber: 1, CompLevel: "quals", Alliance: "B", AlliancePosition: 3, TeamNumber: 4464},
+			MatchNumber: 9, SetNumber: 1, CompLevel: "quals", Alliance: "B", AlliancePosition: 3, TeamNumber: "4464"},
 		TeamMatch{
-			MatchNumber: 8, SetNumber: 1, CompLevel: "quals", Alliance: "R", AlliancePosition: 2, TeamNumber: 2352},
+			MatchNumber: 8, SetNumber: 1, CompLevel: "quals", Alliance: "R", AlliancePosition: 2, TeamNumber: "2352"},
 		TeamMatch{
-			MatchNumber: 9, SetNumber: 1, CompLevel: "quals", Alliance: "B", AlliancePosition: 3, TeamNumber: 6321},
+			MatchNumber: 9, SetNumber: 1, CompLevel: "quals", Alliance: "B", AlliancePosition: 3, TeamNumber: "6321"},
 	}
 
 	for i := 0; i < len(testDatabase); i++ {
@@ -803,9 +802,9 @@
 
 	correct := []TeamMatch{
 		TeamMatch{
-			MatchNumber: 8, SetNumber: 1, CompLevel: "quals", Alliance: "R", AlliancePosition: 2, TeamNumber: 2352},
+			MatchNumber: 8, SetNumber: 1, CompLevel: "quals", Alliance: "R", AlliancePosition: 2, TeamNumber: "2352"},
 		TeamMatch{
-			MatchNumber: 9, SetNumber: 1, CompLevel: "quals", Alliance: "B", AlliancePosition: 3, TeamNumber: 6321},
+			MatchNumber: 9, SetNumber: 1, CompLevel: "quals", Alliance: "B", AlliancePosition: 3, TeamNumber: "6321"},
 	}
 
 	got, err := fixture.db.ReturnMatches()
@@ -940,17 +939,17 @@
 
 	matches := []TeamMatch{
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 1235},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "1235"},
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 2, TeamNumber: 1236},
+			Alliance: "R", AlliancePosition: 2, TeamNumber: "1236"},
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 3, TeamNumber: 1237},
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "1237"},
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 1, TeamNumber: 1238},
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "1238"},
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 2, TeamNumber: 1239},
+			Alliance: "B", AlliancePosition: 2, TeamNumber: "1239"},
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 3, TeamNumber: 1233},
+			Alliance: "B", AlliancePosition: 3, TeamNumber: "1233"},
 	}
 
 	for _, match := range matches {
@@ -1020,13 +1019,13 @@
 
 	matches := []TeamMatch{
 		TeamMatch{MatchNumber: 2, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 2343},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "2343"},
 		TeamMatch{MatchNumber: 2, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 2, TeamNumber: 5443},
+			Alliance: "R", AlliancePosition: 2, TeamNumber: "5443"},
 		TeamMatch{MatchNumber: 2, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 3, TeamNumber: 5436},
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "5436"},
 		TeamMatch{MatchNumber: 2, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 1, TeamNumber: 5643},
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "5643"},
 	}
 
 	for _, match := range matches {
@@ -1079,17 +1078,17 @@
 
 	matches := []TeamMatch{
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 1, TeamNumber: 1235},
+			Alliance: "R", AlliancePosition: 1, TeamNumber: "1235"},
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 2, TeamNumber: 1236},
+			Alliance: "R", AlliancePosition: 2, TeamNumber: "1236"},
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "R", AlliancePosition: 3, TeamNumber: 1237},
+			Alliance: "R", AlliancePosition: 3, TeamNumber: "1237"},
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 1, TeamNumber: 1238},
+			Alliance: "B", AlliancePosition: 1, TeamNumber: "1238"},
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 2, TeamNumber: 1239},
+			Alliance: "B", AlliancePosition: 2, TeamNumber: "1239"},
 		TeamMatch{MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			Alliance: "B", AlliancePosition: 3, TeamNumber: 1233},
+			Alliance: "B", AlliancePosition: 3, TeamNumber: "1233"},
 	}
 
 	for _, match := range matches {
diff --git a/scouting/scraping/scrape.go b/scouting/scraping/scrape.go
index 9cb2336..c6aa6f4 100644
--- a/scouting/scraping/scrape.go
+++ b/scouting/scraping/scrape.go
@@ -78,7 +78,7 @@
 
 	defer resp.Body.Close()
 	if resp.StatusCode != 200 {
-		return nil, errors.New(fmt.Sprint("Got unexpected status code from TBA API request: ", resp.Status))
+		return nil, errors.New(fmt.Sprint("Got unexpected status code from TBA API request ", req.URL, ": ", resp.Status))
 	}
 
 	// Get all bytes from response body.
diff --git a/scouting/webserver/main.go b/scouting/webserver/main.go
index 4752cf4..1b5a002 100644
--- a/scouting/webserver/main.go
+++ b/scouting/webserver/main.go
@@ -9,6 +9,7 @@
 	"log"
 	"os"
 	"os/signal"
+	"path"
 	"strconv"
 	"syscall"
 	"time"
@@ -58,6 +59,14 @@
 	return 8080
 }
 
+func getDefaultBlueAllianceConfig() string {
+	workspaceDirectory := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
+	if workspaceDirectory != "" {
+		return path.Join(workspaceDirectory, "scouting_config.json")
+	}
+	return "scouting_config.json"
+}
+
 func main() {
 	portPtr := flag.Int("port", getDefaultPort(), "The port number to bind to.")
 	dirPtr := flag.String("directory", ".", "The directory to serve at /.")
@@ -71,9 +80,11 @@
 			"-db_config.")
 	dbConnectRetries := flag.Int("db_retries", 5,
 		"The number of seconds to retry connecting to the database on startup.")
-	blueAllianceConfigPtr := flag.String("tba_config", "",
+	blueAllianceConfigPtr := flag.String("tba_config", getDefaultBlueAllianceConfig(),
 		"The path to your The Blue Alliance JSON config. "+
 			"It needs an \"api_key\" field with your TBA API key. "+
+			"It needs a \"year\" field with the event year. "+
+			"It needs an \"event_code\" field with the event code. "+
 			"Optionally, it can have a \"base_url\" field with the TBA API base URL.")
 	flag.Parse()
 
diff --git a/scouting/webserver/match_list/match_list.go b/scouting/webserver/match_list/match_list.go
index 9029438..c5af661 100644
--- a/scouting/webserver/match_list/match_list.go
+++ b/scouting/webserver/match_list/match_list.go
@@ -97,32 +97,32 @@
 			{
 				MatchNumber: int32(match.MatchNumber),
 				SetNumber:   int32(match.SetNumber), CompLevel: match.CompLevel,
-				Alliance: "R", AlliancePosition: 1, TeamNumber: red[0],
+				Alliance: "R", AlliancePosition: 1, TeamNumber: strconv.Itoa(int(red[0])),
 			},
 			{
 				MatchNumber: int32(match.MatchNumber),
 				SetNumber:   int32(match.SetNumber), CompLevel: match.CompLevel,
-				Alliance: "R", AlliancePosition: 2, TeamNumber: red[1],
+				Alliance: "R", AlliancePosition: 2, TeamNumber: strconv.Itoa(int(red[1])),
 			},
 			{
 				MatchNumber: int32(match.MatchNumber),
 				SetNumber:   int32(match.SetNumber), CompLevel: match.CompLevel,
-				Alliance: "R", AlliancePosition: 3, TeamNumber: red[2],
+				Alliance: "R", AlliancePosition: 3, TeamNumber: strconv.Itoa(int(red[2])),
 			},
 			{
 				MatchNumber: int32(match.MatchNumber),
 				SetNumber:   int32(match.SetNumber), CompLevel: match.CompLevel,
-				Alliance: "B", AlliancePosition: 1, TeamNumber: blue[0],
+				Alliance: "B", AlliancePosition: 1, TeamNumber: strconv.Itoa(int(blue[0])),
 			},
 			{
 				MatchNumber: int32(match.MatchNumber),
 				SetNumber:   int32(match.SetNumber), CompLevel: match.CompLevel,
-				Alliance: "B", AlliancePosition: 2, TeamNumber: blue[1],
+				Alliance: "B", AlliancePosition: 2, TeamNumber: strconv.Itoa(int(blue[1])),
 			},
 			{
 				MatchNumber: int32(match.MatchNumber),
 				SetNumber:   int32(match.SetNumber), CompLevel: match.CompLevel,
-				Alliance: "B", AlliancePosition: 3, TeamNumber: blue[2],
+				Alliance: "B", AlliancePosition: 3, TeamNumber: strconv.Itoa(int(blue[2])),
 			},
 		}
 
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
index 4c4870c..935d721 100644
--- a/scouting/webserver/requests/BUILD
+++ b/scouting/webserver/requests/BUILD
@@ -60,6 +60,7 @@
         "//scouting/webserver/requests/messages:request_notes_for_team_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_actions_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_driver_ranking_go_fbs",
diff --git a/scouting/webserver/requests/messages/request_2023_data_scouting_response.fbs b/scouting/webserver/requests/messages/request_2023_data_scouting_response.fbs
index d9d36b3..93583ce 100644
--- a/scouting/webserver/requests/messages/request_2023_data_scouting_response.fbs
+++ b/scouting/webserver/requests/messages/request_2023_data_scouting_response.fbs
@@ -24,7 +24,8 @@
   middle_cones:int (id:16);
   high_cones:int (id:17);
   cones_dropped:int (id:18);
-  avg_cycle:int (id:19);
+  // Time in nanoseconds.
+  avg_cycle:int64 (id:19);
 
   collected_by:string (id:20);
 }
@@ -33,4 +34,4 @@
     stats_list:[Stats2023] (id:0);
 }
 
-root_type Request2023DataScoutingResponse;
\ No newline at end of file
+root_type Request2023DataScoutingResponse;
diff --git a/scouting/webserver/requests/messages/request_all_matches_response.fbs b/scouting/webserver/requests/messages/request_all_matches_response.fbs
index 9d3be62..55da7bb 100644
--- a/scouting/webserver/requests/messages/request_all_matches_response.fbs
+++ b/scouting/webserver/requests/messages/request_all_matches_response.fbs
@@ -1,15 +1,28 @@
 namespace scouting.webserver.requests;
 
+// Specifies whether a team has been scouted for this particular match.
+table ScoutedLevel {
+    r1: bool (id: 0);
+    r2: bool (id: 1);
+    r3: bool (id: 2);
+    b1: bool (id: 3);
+    b2: bool (id: 4);
+    b3: bool (id: 5);
+}
+
 table Match {
     match_number:int (id: 0);
     set_number:int (id: 1);
     comp_level:string (id: 2);
-    r1:int (id: 3);
-    r2:int (id: 4);
-    r3:int (id: 5);
-    b1:int (id: 6);
-    b2:int (id: 7);
-    b3:int (id: 8);
+    r1:string (id: 3);
+    r2:string (id: 4);
+    r3:string (id: 5);
+    b1:string (id: 6);
+    b2:string (id: 7);
+    b3:string (id: 8);
+
+    // Tells you how completely we've data scouted this match.
+    data_scouted: ScoutedLevel (id: 9);
 }
 
 table RequestAllMatchesResponse  {
diff --git a/scouting/webserver/requests/messages/submit_actions.fbs b/scouting/webserver/requests/messages/submit_actions.fbs
index dfb980f..5488a79 100644
--- a/scouting/webserver/requests/messages/submit_actions.fbs
+++ b/scouting/webserver/requests/messages/submit_actions.fbs
@@ -50,12 +50,15 @@
 }
 
 table Action {
-    timestamp:int (id:0);
+    timestamp:int64 (id:0);
     action_taken:ActionType (id:2);
 }
 
 table SubmitActions {
-    actions_list:[Action] (id:0);
+    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);
+    collected_by:string (id: 5);
 }
-
-root_type SubmitActions;
\ No newline at end of file
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 467542a..3aa9076 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -79,6 +79,7 @@
 	ReturnAllShifts() ([]db.Shift, error)
 	ReturnStats() ([]db.Stats, error)
 	ReturnStats2023() ([]db.Stats2023, error)
+	ReturnStats2023ForTeam(teamNumber string, matchNumber int32, setNumber int32, compLevel string) ([]db.Stats2023, error)
 	QueryAllShifts(int) ([]db.Shift, error)
 	QueryStats(int) ([]db.Stats, error)
 	QueryNotes(int32) ([]string, error)
@@ -212,6 +213,16 @@
 	db Database
 }
 
+// Change structure of match objects in the database(1 per team) to
+// the old match structure(1 per match) that the webserver uses.
+// We use the information in this struct to identify which match object
+// corresponds to which old match structure object.
+type MatchAssemblyKey struct {
+	MatchNumber int32
+	SetNumber   int32
+	CompLevel   string
+}
+
 func findIndexInList(list []string, comp_level string) (int, error) {
 	for index, value := range list {
 		if value == comp_level {
@@ -221,6 +232,15 @@
 	return -1, errors.New(fmt.Sprint("Failed to find comp level ", comp_level, " in list ", list))
 }
 
+func (handler requestAllMatchesHandler) teamHasBeenDataScouted(key MatchAssemblyKey, teamNumber string) (bool, error) {
+	stats, err := handler.db.ReturnStats2023ForTeam(
+		teamNumber, key.MatchNumber, key.SetNumber, key.CompLevel)
+	if err != nil {
+		return false, err
+	}
+	return (len(stats) > 0), nil
+}
+
 func (handler requestAllMatchesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	requestBytes, err := io.ReadAll(req.Body)
 	if err != nil {
@@ -239,35 +259,42 @@
 		return
 	}
 
-	// Change structure of match objects in the database(1 per team) to
-	// the old match structure(1 per match) that the webserver uses.
-	type Key struct {
-		MatchNumber int32
-		SetNumber   int32
-		CompLevel   string
-	}
-
-	assembledMatches := map[Key]request_all_matches_response.MatchT{}
+	assembledMatches := map[MatchAssemblyKey]request_all_matches_response.MatchT{}
 
 	for _, match := range matches {
-		key := Key{match.MatchNumber, match.SetNumber, match.CompLevel}
+		key := MatchAssemblyKey{match.MatchNumber, match.SetNumber, match.CompLevel}
+
+		// Retrieve the converted match structure we have assembled so
+		// far. If we haven't started assembling one yet, then start a
+		// new one.
 		entry, ok := assembledMatches[key]
 		if !ok {
 			entry = request_all_matches_response.MatchT{
 				MatchNumber: match.MatchNumber,
 				SetNumber:   match.SetNumber,
 				CompLevel:   match.CompLevel,
+				DataScouted: &request_all_matches_response.ScoutedLevelT{},
 			}
 		}
+
+		var team *string
+		var dataScoutedTeam *bool
+
+		// Fill in the field for the match that we have in in the
+		// database. In the database, each match row only has 1 team
+		// number.
 		switch match.Alliance {
 		case "R":
 			switch match.AlliancePosition {
 			case 1:
-				entry.R1 = match.TeamNumber
+				team = &entry.R1
+				dataScoutedTeam = &entry.DataScouted.R1
 			case 2:
-				entry.R2 = match.TeamNumber
+				team = &entry.R2
+				dataScoutedTeam = &entry.DataScouted.R2
 			case 3:
-				entry.R3 = match.TeamNumber
+				team = &entry.R3
+				dataScoutedTeam = &entry.DataScouted.R3
 			default:
 				respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown red position ", strconv.Itoa(int(match.AlliancePosition)), " in match ", strconv.Itoa(int(match.MatchNumber))))
 				return
@@ -275,11 +302,14 @@
 		case "B":
 			switch match.AlliancePosition {
 			case 1:
-				entry.B1 = match.TeamNumber
+				team = &entry.B1
+				dataScoutedTeam = &entry.DataScouted.B1
 			case 2:
-				entry.B2 = match.TeamNumber
+				team = &entry.B2
+				dataScoutedTeam = &entry.DataScouted.B2
 			case 3:
-				entry.B3 = match.TeamNumber
+				team = &entry.B3
+				dataScoutedTeam = &entry.DataScouted.B3
 			default:
 				respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown blue position ", strconv.Itoa(int(match.AlliancePosition)), " in match ", strconv.Itoa(int(match.MatchNumber))))
 				return
@@ -288,6 +318,21 @@
 			respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown alliance ", match.Alliance, " in match ", strconv.Itoa(int(match.AlliancePosition))))
 			return
 		}
+
+		*team = match.TeamNumber
+
+		// Figure out if this team has been data scouted already.
+		*dataScoutedTeam, err = handler.teamHasBeenDataScouted(key, match.TeamNumber)
+		if err != nil {
+			respondWithError(w, http.StatusInternalServerError, fmt.Sprint(
+				"Failed to determine data scouting status for team ",
+				strconv.Itoa(int(match.AlliancePosition)),
+				" in match ",
+				strconv.Itoa(int(match.MatchNumber)),
+				err))
+			return
+		}
+
 		assembledMatches[key] = entry
 	}
 
@@ -454,6 +499,104 @@
 	w.Write(builder.FinishedBytes())
 }
 
+func ConvertActionsToStat(submitActions *submit_actions.SubmitActions) (db.Stats2023, error) {
+	overall_time := int64(0)
+	cycles := int64(0)
+	picked_up := false
+	lastPlacedTime := int64(0)
+	stat := db.Stats2023{TeamNumber: string(submitActions.TeamNumber()), MatchNumber: submitActions.MatchNumber(), SetNumber: submitActions.SetNumber(), CompLevel: string(submitActions.CompLevel()),
+		StartingQuadrant: 0, LowCubesAuto: 0, MiddleCubesAuto: 0, HighCubesAuto: 0, CubesDroppedAuto: 0,
+		LowConesAuto: 0, MiddleConesAuto: 0, HighConesAuto: 0, ConesDroppedAuto: 0, LowCubes: 0, MiddleCubes: 0, HighCubes: 0,
+		CubesDropped: 0, LowCones: 0, MiddleCones: 0, HighCones: 0, ConesDropped: 0, AvgCycle: 0, CollectedBy: string(submitActions.CollectedBy()),
+	}
+	// Loop over all actions.
+	for i := 0; i < submitActions.ActionsListLength(); i++ {
+		var action submit_actions.Action
+		if !submitActions.ActionsList(&action, i) {
+			return db.Stats2023{}, errors.New(fmt.Sprintf("Failed to parse submit_actions.Action"))
+		}
+		actionTable := new(flatbuffers.Table)
+		action_type := action.ActionTakenType()
+		if !action.ActionTaken(actionTable) {
+			return db.Stats2023{}, errors.New(fmt.Sprint("Failed to parse sub-action or sub-action was missing"))
+		}
+		if action_type == submit_actions.ActionTypeStartMatchAction {
+			var startMatchAction submit_actions.StartMatchAction
+			startMatchAction.Init(actionTable.Bytes, actionTable.Pos)
+			stat.StartingQuadrant = startMatchAction.Position()
+		} else if action_type == submit_actions.ActionTypePickupObjectAction {
+			var pick_up_action submit_actions.PickupObjectAction
+			pick_up_action.Init(actionTable.Bytes, actionTable.Pos)
+			if picked_up == true {
+				object := pick_up_action.ObjectType().String()
+				auto := pick_up_action.Auto()
+				if object == "kCube" && auto == false {
+					stat.CubesDropped += 1
+				} else if object == "kCube" && auto == true {
+					stat.CubesDroppedAuto += 1
+				} else if object == "kCone" && auto == false {
+					stat.ConesDropped += 1
+				} else if object == "kCube" && auto == true {
+					stat.ConesDroppedAuto += 1
+				}
+			} else {
+				picked_up = true
+			}
+		} else if action_type == submit_actions.ActionTypePlaceObjectAction {
+			var place_action submit_actions.PlaceObjectAction
+			place_action.Init(actionTable.Bytes, actionTable.Pos)
+			if !picked_up {
+				return db.Stats2023{}, errors.New(fmt.Sprintf("Got PlaceObjectAction without corresponding PickupObjectAction"))
+			}
+			object := place_action.ObjectType()
+			level := place_action.ScoreLevel()
+			auto := place_action.Auto()
+			if object == 0 && level == 0 && auto == true {
+				stat.LowCubesAuto += 1
+			} else if object == 0 && level == 0 && auto == false {
+				stat.LowCubes += 1
+			} else if object == 0 && level == 1 && auto == true {
+				stat.MiddleCubesAuto += 1
+			} else if object == 0 && level == 1 && auto == false {
+				stat.MiddleCubes += 1
+			} else if object == 0 && level == 2 && auto == true {
+				stat.HighCubesAuto += 1
+			} else if object == 0 && level == 2 && auto == false {
+				stat.HighCubes += 1
+			} else if object == 1 && level == 0 && auto == true {
+				stat.LowConesAuto += 1
+			} else if object == 1 && level == 0 && auto == false {
+				stat.LowCones += 1
+			} else if object == 1 && level == 1 && auto == true {
+				stat.MiddleConesAuto += 1
+			} else if object == 1 && level == 1 && auto == false {
+				stat.MiddleCones += 1
+			} else if object == 1 && level == 2 && auto == true {
+				stat.HighConesAuto += 1
+			} else if object == 1 && level == 2 && auto == false {
+				stat.HighCones += 1
+			} else {
+				return db.Stats2023{}, 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())
+		}
+	}
+	if cycles != 0 {
+		stat.AvgCycle = overall_time / cycles
+	} else {
+		stat.AvgCycle = 0
+	}
+	return stat, nil
+}
+
 // Handles a Request2023DataScouting request.
 type request2023DataScoutingHandler struct {
 	db Database
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index efd770b..50d6820 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -23,6 +23,7 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team"
 	"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_actions"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking"
@@ -127,75 +128,98 @@
 		matches: []db.TeamMatch{
 			{
 				MatchNumber: 1, SetNumber: 1, CompLevel: "qm",
-				Alliance: "R", AlliancePosition: 1, TeamNumber: 5,
+				Alliance: "R", AlliancePosition: 1, TeamNumber: "5",
 			},
 			{
 				MatchNumber: 1, SetNumber: 1, CompLevel: "qm",
-				Alliance: "R", AlliancePosition: 2, TeamNumber: 42,
+				Alliance: "R", AlliancePosition: 2, TeamNumber: "42",
 			},
 			{
 				MatchNumber: 1, SetNumber: 1, CompLevel: "qm",
-				Alliance: "R", AlliancePosition: 3, TeamNumber: 600,
+				Alliance: "R", AlliancePosition: 3, TeamNumber: "600",
 			},
 			{
 				MatchNumber: 1, SetNumber: 1, CompLevel: "qm",
-				Alliance: "B", AlliancePosition: 1, TeamNumber: 971,
+				Alliance: "B", AlliancePosition: 1, TeamNumber: "971",
 			},
 			{
 				MatchNumber: 1, SetNumber: 1, CompLevel: "qm",
-				Alliance: "B", AlliancePosition: 2, TeamNumber: 400,
+				Alliance: "B", AlliancePosition: 2, TeamNumber: "400",
 			},
 			{
 				MatchNumber: 1, SetNumber: 1, CompLevel: "qm",
-				Alliance: "B", AlliancePosition: 3, TeamNumber: 200,
+				Alliance: "B", AlliancePosition: 3, TeamNumber: "200",
 			},
 			{
 				MatchNumber: 2, SetNumber: 1, CompLevel: "qm",
-				Alliance: "R", AlliancePosition: 1, TeamNumber: 6,
+				Alliance: "R", AlliancePosition: 1, TeamNumber: "6",
 			},
 			{
 				MatchNumber: 2, SetNumber: 1, CompLevel: "qm",
-				Alliance: "R", AlliancePosition: 2, TeamNumber: 43,
+				Alliance: "R", AlliancePosition: 2, TeamNumber: "43",
 			},
 			{
 				MatchNumber: 2, SetNumber: 1, CompLevel: "qm",
-				Alliance: "R", AlliancePosition: 3, TeamNumber: 601,
+				Alliance: "R", AlliancePosition: 3, TeamNumber: "601",
 			},
 			{
 				MatchNumber: 2, SetNumber: 1, CompLevel: "qm",
-				Alliance: "B", AlliancePosition: 1, TeamNumber: 972,
+				Alliance: "B", AlliancePosition: 1, TeamNumber: "972",
 			},
 			{
 				MatchNumber: 2, SetNumber: 1, CompLevel: "qm",
-				Alliance: "B", AlliancePosition: 2, TeamNumber: 401,
+				Alliance: "B", AlliancePosition: 2, TeamNumber: "401",
 			},
 			{
 				MatchNumber: 2, SetNumber: 1, CompLevel: "qm",
-				Alliance: "B", AlliancePosition: 3, TeamNumber: 201,
+				Alliance: "B", AlliancePosition: 3, TeamNumber: "201",
 			},
 			{
 				MatchNumber: 3, SetNumber: 1, CompLevel: "qm",
-				Alliance: "R", AlliancePosition: 1, TeamNumber: 7,
+				Alliance: "R", AlliancePosition: 1, TeamNumber: "7",
 			},
 			{
 				MatchNumber: 3, SetNumber: 1, CompLevel: "qm",
-				Alliance: "R", AlliancePosition: 2, TeamNumber: 44,
+				Alliance: "R", AlliancePosition: 2, TeamNumber: "44",
 			},
 			{
 				MatchNumber: 3, SetNumber: 1, CompLevel: "qm",
-				Alliance: "R", AlliancePosition: 3, TeamNumber: 602,
+				Alliance: "R", AlliancePosition: 3, TeamNumber: "602",
 			},
 			{
 				MatchNumber: 3, SetNumber: 1, CompLevel: "qm",
-				Alliance: "B", AlliancePosition: 1, TeamNumber: 973,
+				Alliance: "B", AlliancePosition: 1, TeamNumber: "973",
 			},
 			{
 				MatchNumber: 3, SetNumber: 1, CompLevel: "qm",
-				Alliance: "B", AlliancePosition: 2, TeamNumber: 402,
+				Alliance: "B", AlliancePosition: 2, TeamNumber: "402",
 			},
 			{
 				MatchNumber: 3, SetNumber: 1, CompLevel: "qm",
-				Alliance: "B", AlliancePosition: 3, TeamNumber: 202,
+				Alliance: "B", AlliancePosition: 3, TeamNumber: "202",
+			},
+		},
+		// Pretend that we have some data scouting data.
+		stats2023: []db.Stats2023{
+			{
+				TeamNumber: "5", MatchNumber: 1, SetNumber: 1,
+				CompLevel: "qm", StartingQuadrant: 3, LowCubesAuto: 10,
+				MiddleCubesAuto: 1, HighCubesAuto: 1, CubesDroppedAuto: 0,
+				LowConesAuto: 1, MiddleConesAuto: 2, HighConesAuto: 1,
+				ConesDroppedAuto: 0, LowCubes: 1, MiddleCubes: 1,
+				HighCubes: 2, CubesDropped: 1, LowCones: 1,
+				MiddleCones: 2, HighCones: 0, ConesDropped: 1,
+				AvgCycle: 34, CollectedBy: "alex",
+			},
+			{
+				TeamNumber: "973", MatchNumber: 3, SetNumber: 1,
+				CompLevel: "qm", StartingQuadrant: 1, LowCubesAuto: 0,
+				MiddleCubesAuto: 1, HighCubesAuto: 1, CubesDroppedAuto: 2,
+				LowConesAuto: 0, MiddleConesAuto: 0, HighConesAuto: 0,
+				ConesDroppedAuto: 1, LowCubes: 0, MiddleCubes: 0,
+				HighCubes: 1, CubesDropped: 0, LowCones: 0,
+				MiddleCones: 2, HighCones: 1, ConesDropped: 1,
+				AvgCycle: 53, CollectedBy: "bob",
 			},
 		},
 	}
@@ -218,15 +242,28 @@
 			// R1, R2, R3, B1, B2, B3
 			{
 				1, 1, "qm",
-				5, 42, 600, 971, 400, 200,
+				"5", "42", "600", "971", "400", "200",
+				&request_all_matches_response.ScoutedLevelT{
+					// The R1 team has already been data
+					// scouted.
+					true, false, false, false, false, false,
+				},
 			},
 			{
 				2, 1, "qm",
-				6, 43, 601, 972, 401, 201,
+				"6", "43", "601", "972", "401", "201",
+				&request_all_matches_response.ScoutedLevelT{
+					false, false, false, false, false, false,
+				},
 			},
 			{
 				3, 1, "qm",
-				7, 44, 602, 973, 402, 202,
+				"7", "44", "602", "973", "402", "202",
+				&request_all_matches_response.ScoutedLevelT{
+					// The B1 team has already been data
+					// scouted.
+					false, false, false, true, false, false,
+				},
 			},
 		},
 	}
@@ -391,6 +428,103 @@
 	}
 }
 
+// Validates that we can request the 2023 stats.
+func TestConvertActionsToStat(t *testing.T) {
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&submit_actions.SubmitActionsT{
+		TeamNumber:  "4244",
+		MatchNumber: 3,
+		SetNumber:   1,
+		CompLevel:   "quals",
+		CollectedBy: "katie",
+		ActionsList: []*submit_actions.ActionT{
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypeStartMatchAction,
+					Value: &submit_actions.StartMatchActionT{
+						Position: 1,
+					},
+				},
+				Timestamp: 0,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePickupObjectAction,
+					Value: &submit_actions.PickupObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCube,
+						Auto:       true,
+					},
+				},
+				Timestamp: 400,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePickupObjectAction,
+					Value: &submit_actions.PickupObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCube,
+						Auto:       true,
+					},
+				},
+				Timestamp: 800,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePlaceObjectAction,
+					Value: &submit_actions.PlaceObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCube,
+						ScoreLevel: submit_actions.ScoreLevelkLow,
+						Auto:       true,
+					},
+				},
+				Timestamp: 2000,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePickupObjectAction,
+					Value: &submit_actions.PickupObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCone,
+						Auto:       false,
+					},
+				},
+				Timestamp: 2800,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePlaceObjectAction,
+					Value: &submit_actions.PlaceObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCone,
+						ScoreLevel: submit_actions.ScoreLevelkHigh,
+						Auto:       false,
+					},
+				},
+				Timestamp: 3100,
+			},
+		},
+	}).Pack(builder))
+
+	submitActions := submit_actions.GetRootAsSubmitActions(builder.FinishedBytes(), 0)
+	response, err := ConvertActionsToStat(submitActions)
+
+	if err != nil {
+		t.Fatal("Failed to convert actions to stats: ", err)
+	}
+
+	expected := db.Stats2023{
+		TeamNumber: "4244", MatchNumber: 3, SetNumber: 1,
+		CompLevel: "quals", StartingQuadrant: 1, LowCubesAuto: 1,
+		MiddleCubesAuto: 0, HighCubesAuto: 0, CubesDroppedAuto: 1,
+		LowConesAuto: 0, MiddleConesAuto: 0, HighConesAuto: 0,
+		ConesDroppedAuto: 0, LowCubes: 0, MiddleCubes: 0,
+		HighCubes: 0, CubesDropped: 0, LowCones: 0,
+		MiddleCones: 0, HighCones: 1, ConesDropped: 0,
+		AvgCycle: 1100, CollectedBy: "katie",
+	}
+
+	if expected != response {
+		t.Fatal("Expected ", expected, ", but got ", response)
+	}
+}
+
 func TestSubmitNotes(t *testing.T) {
 	database := MockDatabase{}
 	scoutingServer := server.NewScoutingServer()
@@ -776,6 +910,16 @@
 	return database.stats2023, nil
 }
 
+func (database *MockDatabase) ReturnStats2023ForTeam(teamNumber string, matchNumber int32, setNumber int32, compLevel string) ([]db.Stats2023, error) {
+	var results []db.Stats2023
+	for _, stats := range database.stats2023 {
+		if stats.TeamNumber == teamNumber && stats.MatchNumber == matchNumber && stats.SetNumber == setNumber && stats.CompLevel == compLevel {
+			results = append(results, stats)
+		}
+	}
+	return results, nil
+}
+
 func (database *MockDatabase) QueryStats(int) ([]db.Stats, error) {
 	return []db.Stats{}, nil
 }
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index 44fc958..aef97f7 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -118,7 +118,6 @@
   matchStartTimestamp: number = 0;
 
   addAction(action: ActionT): void {
-    action.timestamp = Math.floor(Date.now() / 1000);
     if (action.type == 'startMatchAction') {
       // Unix nanosecond timestamp.
       this.matchStartTimestamp = Date.now() * 1e6;
@@ -193,7 +192,7 @@
             StartMatchAction.createStartMatchAction(builder, action.position);
           actionOffset = Action.createAction(
             builder,
-            action.timestamp || 0,
+            BigInt(action.timestamp || 0),
             ActionType.StartMatchAction,
             startMatchActionOffset
           );
@@ -208,7 +207,7 @@
             );
           actionOffset = Action.createAction(
             builder,
-            action.timestamp || 0,
+            BigInt(action.timestamp || 0),
             ActionType.PickupObjectAction,
             pickupObjectActionOffset
           );
@@ -223,7 +222,7 @@
             );
           actionOffset = Action.createAction(
             builder,
-            action.timestamp || 0,
+            BigInt(action.timestamp || 0),
             ActionType.AutoBalanceAction,
             autoBalanceActionOffset
           );
@@ -239,7 +238,7 @@
             );
           actionOffset = Action.createAction(
             builder,
-            action.timestamp || 0,
+            BigInt(action.timestamp || 0),
             ActionType.PlaceObjectAction,
             placeObjectActionOffset
           );
@@ -250,7 +249,7 @@
             RobotDeathAction.createRobotDeathAction(builder, action.robotOn);
           actionOffset = Action.createAction(
             builder,
-            action.timestamp || 0,
+            BigInt(action.timestamp || 0),
             ActionType.RobotDeathAction,
             robotDeathActionOffset
           );
@@ -264,7 +263,7 @@
           );
           actionOffset = Action.createAction(
             builder,
-            action.timestamp || 0,
+            BigInt(action.timestamp || 0),
             ActionType.EndMatchAction,
             endMatchActionOffset
           );
diff --git a/scouting/www/match_list/match_list.component.css b/scouting/www/match_list/match_list.component.css
index e7c071c..f77be5e 100644
--- a/scouting/www/match_list/match_list.component.css
+++ b/scouting/www/match_list/match_list.component.css
@@ -6,6 +6,10 @@
   background-color: #dc3545;
 }
 
+button:disabled {
+  background-color: #524143;
+}
+
 .blue {
   background-color: #0d6efd;
 }
@@ -22,3 +26,7 @@
   /* minimum touch target size */
   height: 44px;
 }
+
+div.hidden_row {
+  display: none;
+}
diff --git a/scouting/www/match_list/match_list.component.ts b/scouting/www/match_list/match_list.component.ts
index eb5284f..0deeb11 100644
--- a/scouting/www/match_list/match_list.component.ts
+++ b/scouting/www/match_list/match_list.component.ts
@@ -10,7 +10,7 @@
 import {MatchListRequestor} from '@org_frc971/scouting/www/rpc';
 
 type TeamInMatch = {
-  teamNumber: number;
+  teamNumber: string;
   matchNumber: number;
   setNumber: number;
   compLevel: string;
@@ -26,21 +26,85 @@
   progressMessage: string = '';
   errorMessage: string = '';
   matchList: Match[] = [];
+  hideCompletedMatches: boolean = true;
 
   constructor(private readonly matchListRequestor: MatchListRequestor) {}
 
+  // Returns true if the match is fully scouted. Returns false otherwise.
+  matchIsFullyScouted(match: Match): boolean {
+    const scouted = match.dataScouted();
+    return (
+      scouted.r1() &&
+      scouted.r2() &&
+      scouted.r3() &&
+      scouted.b1() &&
+      scouted.b2() &&
+      scouted.b3()
+    );
+  }
+
+  // Returns true if at least one team in this match has been scouted. Returns
+  // false otherwise.
+  matchIsPartiallyScouted(match: Match): boolean {
+    const scouted = match.dataScouted();
+    return (
+      scouted.r1() ||
+      scouted.r2() ||
+      scouted.r3() ||
+      scouted.b1() ||
+      scouted.b2() ||
+      scouted.b3()
+    );
+  }
+
+  // Returns a class for the row to hide it if all teams in this match have
+  // already been scouted.
+  getRowClass(match: Match): string {
+    if (this.hideCompletedMatches && this.matchIsFullyScouted(match)) {
+      return 'hidden_row';
+    }
+    return '';
+  }
+
   setTeamInMatch(teamInMatch: TeamInMatch) {
     this.selectedTeamEvent.emit(teamInMatch);
   }
 
-  teamsInMatch(match: Match): {teamNumber: number; color: 'red' | 'blue'}[] {
+  teamsInMatch(
+    match: Match
+  ): {teamNumber: string; color: 'red' | 'blue'; disabled: boolean}[] {
+    const scouted = match.dataScouted();
     return [
-      {teamNumber: match.r1(), color: 'red'},
-      {teamNumber: match.r2(), color: 'red'},
-      {teamNumber: match.r3(), color: 'red'},
-      {teamNumber: match.b1(), color: 'blue'},
-      {teamNumber: match.b2(), color: 'blue'},
-      {teamNumber: match.b3(), color: 'blue'},
+      {
+        teamNumber: match.r1(),
+        color: 'red',
+        disabled: this.hideCompletedMatches && scouted.r1(),
+      },
+      {
+        teamNumber: match.r2(),
+        color: 'red',
+        disabled: this.hideCompletedMatches && scouted.r2(),
+      },
+      {
+        teamNumber: match.r3(),
+        color: 'red',
+        disabled: this.hideCompletedMatches && scouted.r3(),
+      },
+      {
+        teamNumber: match.b1(),
+        color: 'blue',
+        disabled: this.hideCompletedMatches && scouted.b1(),
+      },
+      {
+        teamNumber: match.b2(),
+        color: 'blue',
+        disabled: this.hideCompletedMatches && scouted.b2(),
+      },
+      {
+        teamNumber: match.b3(),
+        color: 'blue',
+        disabled: this.hideCompletedMatches && scouted.b3(),
+      },
     ];
   }
 
@@ -64,7 +128,22 @@
   displayMatchNumber(match: Match): string {
     // Only display the set number for eliminations matches.
     const setNumber = match.compLevel() == 'qm' ? '' : `${match.setNumber()}`;
-    return `${this.matchType(match)} ${setNumber} Match ${match.matchNumber()}`;
+    const matchType = this.matchType(match);
+    const mainText = `${matchType} ${setNumber} Match ${match.matchNumber()}`;
+
+    // When showing the full match list (i.e. not hiding completed matches)
+    // it's useful to know if a match has already been scouted or not.
+    const suffix = (() => {
+      if (this.matchIsFullyScouted(match)) {
+        return '(fully scouted)';
+      } else if (this.matchIsPartiallyScouted(match)) {
+        return '(partially scouted)';
+      } else {
+        return '';
+      }
+    })();
+
+    return `${mainText} ${suffix}`;
   }
 
   ngOnInit() {
diff --git a/scouting/www/match_list/match_list.ng.html b/scouting/www/match_list/match_list.ng.html
index e890faf..0ebbe4c 100644
--- a/scouting/www/match_list/match_list.ng.html
+++ b/scouting/www/match_list/match_list.ng.html
@@ -2,8 +2,16 @@
   <h2>Matches</h2>
 </div>
 
+<label>
+  <input type="checkbox" [(ngModel)]="hideCompletedMatches" />
+  Hide completed matches
+</label>
+
 <div class="container-fluid">
-  <div class="row" *ngFor="let match of matchList; index as i">
+  <div
+    *ngFor="let match of matchList; index as i"
+    [ngClass]="'row ' + getRowClass(match)"
+  >
     <span class="badge bg-secondary rounded-left">
       {{ displayMatchNumber(match) }}
     </span>
@@ -18,6 +26,7 @@
             })"
         class="match-item"
         [ngClass]="team.color"
+        [disabled]="team.disabled"
       >
         {{ team.teamNumber }}
       </button>
diff --git a/scouting/www/notes/notes.component.ts b/scouting/www/notes/notes.component.ts
index 21264f2..94a1586 100644
--- a/scouting/www/notes/notes.component.ts
+++ b/scouting/www/notes/notes.component.ts
@@ -111,7 +111,7 @@
   setTeamNumber() {
     let data: Input = {
       teamNumber: this.teamNumberSelection,
-      notesData: 'Auto: \nTeleop: \nEngame: ',
+      notesData: 'Match: \nAuto: \nTeleop: \nEngame: ',
       keywordsData: {
         goodDriving: false,
         badDriving: false,
diff --git a/y2023/vision/aprilrobotics.cc b/y2023/vision/aprilrobotics.cc
index 45b0cf9..95ad541 100644
--- a/y2023/vision/aprilrobotics.cc
+++ b/y2023/vision/aprilrobotics.cc
@@ -36,7 +36,8 @@
       target_map_sender_(
           event_loop->MakeSender<frc971::vision::TargetMap>("/camera")),
       image_annotations_sender_(
-          event_loop->MakeSender<foxglove::ImageAnnotations>("/camera")) {
+          event_loop->MakeSender<foxglove::ImageAnnotations>("/camera")),
+      rejections_(0) {
   tag_family_ = tag16h5_create();
   tag_detector_ = apriltag_detector_create();
 
@@ -89,17 +90,18 @@
                                         aos::monotonic_clock::time_point eof) {
   image_size_ = image_grayscale.size();
 
-  std::vector<Detection> detections = DetectTags(image_grayscale, eof);
+  DetectionResult result = DetectTags(image_grayscale, eof);
 
   auto builder = target_map_sender_.MakeBuilder();
   std::vector<flatbuffers::Offset<frc971::vision::TargetPoseFbs>> target_poses;
-  for (const auto &detection : detections) {
+  for (const auto &detection : result.detections) {
     target_poses.emplace_back(BuildTargetPose(detection, builder.fbb()));
   }
   const auto target_poses_offset = builder.fbb()->CreateVector(target_poses);
   auto target_map_builder = builder.MakeBuilder<frc971::vision::TargetMap>();
   target_map_builder.add_target_poses(target_poses_offset);
   target_map_builder.add_monotonic_timestamp_ns(eof.time_since_epoch().count());
+  target_map_builder.add_rejections(result.rejections);
   builder.CheckOk(builder.Send(target_map_builder.Finish()));
 }
 
@@ -177,7 +179,7 @@
   return corner_points;
 }
 
-std::vector<AprilRoboticsDetector::Detection> AprilRoboticsDetector::DetectTags(
+AprilRoboticsDetector::DetectionResult AprilRoboticsDetector::DetectTags(
     cv::Mat image, aos::monotonic_clock::time_point eof) {
   const aos::monotonic_clock::time_point start_time =
       aos::monotonic_clock::now();
@@ -273,6 +275,8 @@
                                      .pose = pose,
                                      .pose_error = pose_error,
                                      .distortion_factor = distortion_factor});
+    } else {
+      rejections_++;
     }
   }
 
@@ -292,7 +296,7 @@
   VLOG(1) << "Took " << chrono::duration<double>(end_time - start_time).count()
           << " seconds to detect overall";
 
-  return results;
+  return {.detections = results, .rejections = rejections_};
 }
 
 }  // namespace vision
diff --git a/y2023/vision/aprilrobotics.h b/y2023/vision/aprilrobotics.h
index bf9265b..fab2d30 100644
--- a/y2023/vision/aprilrobotics.h
+++ b/y2023/vision/aprilrobotics.h
@@ -31,6 +31,11 @@
     double distortion_factor;
   };
 
+  struct DetectionResult {
+    std::vector<Detection> detections;
+    size_t rejections;
+  };
+
   AprilRoboticsDetector(aos::EventLoop *event_loop,
                         std::string_view channel_name);
   ~AprilRoboticsDetector();
@@ -43,8 +48,8 @@
   // Helper function to store detection points in vector of Point2f's
   std::vector<cv::Point2f> MakeCornerVector(const apriltag_detection_t *det);
 
-  std::vector<Detection> DetectTags(cv::Mat image,
-                                    aos::monotonic_clock::time_point eof);
+  DetectionResult DetectTags(cv::Mat image,
+                             aos::monotonic_clock::time_point eof);
 
   const std::optional<cv::Mat> extrinsics() const { return extrinsics_; }
   const cv::Mat intrinsics() const { return intrinsics_; }
@@ -78,6 +83,8 @@
   frc971::vision::ImageCallback image_callback_;
   aos::Sender<frc971::vision::TargetMap> target_map_sender_;
   aos::Sender<foxglove::ImageAnnotations> image_annotations_sender_;
+
+  size_t rejections_;
 };
 
 }  // namespace vision
diff --git a/y2023/www/BUILD b/y2023/www/BUILD
index f2df646..404247e 100644
--- a/y2023/www/BUILD
+++ b/y2023/www/BUILD
@@ -35,6 +35,7 @@
         "//frc971/control_loops:control_loops_ts_fbs",
         "//frc971/control_loops/drivetrain:drivetrain_status_ts_fbs",
         "//frc971/control_loops/drivetrain/localization:localizer_output_ts_fbs",
+        "//frc971/vision:target_map_ts_fbs",
         "//y2023/control_loops/superstructure:superstructure_status_ts_fbs",
         "//y2023/localizer:status_ts_fbs",
         "//y2023/localizer:visualization_ts_fbs",
diff --git a/y2023/www/field_handler.ts b/y2023/www/field_handler.ts
index b8656a2..2f62c7d 100644
--- a/y2023/www/field_handler.ts
+++ b/y2023/www/field_handler.ts
@@ -1,14 +1,14 @@
-import {ByteBuffer} from 'flatbuffers';
-
+import {ByteBuffer} from 'flatbuffers'
 import {ClientStatistics} from '../../aos/network/message_bridge_client_generated'
 import {ServerStatistics, State as ConnectionState} from '../../aos/network/message_bridge_server_generated'
-import {Connection} from '../../aos/network/www/proxy';
-import {ZeroingError} from '../../frc971/control_loops/control_loops_generated';
-import {Status as DrivetrainStatus} from '../../frc971/control_loops/drivetrain/drivetrain_status_generated';
-import {LocalizerOutput} from '../../frc971/control_loops/drivetrain/localization/localizer_output_generated';
+import {Connection} from '../../aos/network/www/proxy'
+import {ZeroingError} from '../../frc971/control_loops/control_loops_generated'
+import {Status as DrivetrainStatus} from '../../frc971/control_loops/drivetrain/drivetrain_status_generated'
+import {LocalizerOutput} from '../../frc971/control_loops/drivetrain/localization/localizer_output_generated'
+import {TargetMap} from '../../frc971/vision/target_map_generated'
 import {ArmState, ArmStatus, EndEffectorState, Status as SuperstructureStatus} from '../control_loops/superstructure/superstructure_status_generated'
-import {RejectionReason} from '../localizer/status_generated';
-import {TargetEstimateDebug, Visualization} from '../localizer/visualization_generated';
+import {RejectionReason} from '../localizer/status_generated'
+import {TargetEstimateDebug, Visualization} from '../localizer/visualization_generated'
 import {Class} from '../vision/game_pieces_generated'
 
 import {FIELD_LENGTH, FIELD_WIDTH, FT_TO_M, IN_TO_M} from './constants';
@@ -38,6 +38,10 @@
       (document.getElementById('theta') as HTMLElement);
   private imagesAcceptedCounter: HTMLElement =
       (document.getElementById('images_accepted') as HTMLElement);
+  // HTML elements for rejection reasons for individual pis. Indices
+  // corresponding to RejectionReason enum values will be for those reasons. The
+  // final row will account for images rejected by the aprilrobotics detector
+  // instead of the localizer.
   private rejectionReasonCells: HTMLElement[][] = [];
   private messageBridgeDiv: HTMLElement =
       (document.getElementById('message_bridge_status') as HTMLElement);
@@ -64,8 +68,6 @@
       (document.getElementById('arm_distal') as HTMLElement);
   private zeroingFaults: HTMLElement =
       (document.getElementById('zeroing_faults') as HTMLElement);
-  _
-
   constructor(private readonly connection: Connection) {
     (document.getElementById('field') as HTMLElement).appendChild(this.canvas);
 
@@ -75,7 +77,7 @@
     {
       const row = document.createElement('div');
       const nameCell = document.createElement('div');
-      nameCell.innerHTML = "Rejection Reason";
+      nameCell.innerHTML = 'Rejection Reason';
       row.appendChild(nameCell);
       for (const pi of PIS) {
         const nodeCell = document.createElement('div');
@@ -98,7 +100,25 @@
       for (const pi of PIS) {
         const valueCell = document.createElement('div');
         valueCell.innerHTML = 'NA';
-        this.rejectionReasonCells[this.rejectionReasonCells.length - 1].push(valueCell);
+        this.rejectionReasonCells[this.rejectionReasonCells.length - 1].push(
+            valueCell);
+        row.appendChild(valueCell);
+      }
+      document.getElementById('vision_readouts').appendChild(row);
+    }
+
+    // Add rejection reason row for aprilrobotics rejections.
+    {
+      const row = document.createElement('div');
+      const nameCell = document.createElement('div');
+      nameCell.innerHTML = 'Rejected by aprilrobotics';
+      row.appendChild(nameCell);
+      this.rejectionReasonCells.push([]);
+      for (const pi of PIS) {
+        const valueCell = document.createElement('div');
+        valueCell.innerHTML = 'NA';
+        this.rejectionReasonCells[this.rejectionReasonCells.length - 1].push(
+            valueCell);
         row.appendChild(valueCell);
       }
       document.getElementById('vision_readouts').appendChild(row);
@@ -121,6 +141,13 @@
               this.handleLocalizerDebug(Number(pi), data);
             });
       }
+      for (const pi in PIS) {
+        // Make unreliable to reduce network spam.
+        this.connection.addHandler(
+            '/' + PIS[pi] + '/camera', 'frc971.vision.TargetMap', (data) => {
+              this.handlePiTargetMap(pi, data);
+            });
+      }
       this.connection.addHandler(
           '/drivetrain', 'frc971.control_loops.drivetrain.Status', (data) => {
             this.handleDrivetrainStatus(data);
@@ -151,7 +178,7 @@
     const debug = this.localizerImageMatches.get(now);
 
     if (debug.statistics()) {
-      if (debug.statistics().rejectionReasonsLength() ==
+      if ((debug.statistics().rejectionReasonsLength() + 1) ==
           this.rejectionReasonCells.length) {
         for (let ii = 0; ii < debug.statistics().rejectionReasonsLength();
              ++ii) {
@@ -164,6 +191,13 @@
     }
   }
 
+  private handlePiTargetMap(pi: string, data: Uint8Array): void {
+    const fbBuffer = new ByteBuffer(data);
+    const targetMap = TargetMap.getRootAsTargetMap(fbBuffer);
+    this.rejectionReasonCells[this.rejectionReasonCells.length - 1][pi]
+        .innerHTML = targetMap.rejections().toString();
+  }
+
   private handleLocalizerOutput(data: Uint8Array): void {
     const fbBuffer = new ByteBuffer(data);
     this.localizerOutput = LocalizerOutput.getRootAsLocalizerOutput(fbBuffer);