Merge "Add ability to query falcon configuration dynamically"
diff --git a/aos/containers/BUILD b/aos/containers/BUILD
index 9fdc93f..ee1bf98 100644
--- a/aos/containers/BUILD
+++ b/aos/containers/BUILD
@@ -64,6 +64,30 @@
 )
 
 cc_library(
+    name = "error_list",
+    hdrs = [
+        "error_list.h",
+    ],
+    deps = [
+        ":sized_array",
+        "//aos:flatbuffers",
+    ],
+)
+
+cc_test(
+    name = "error_list_test",
+    srcs = [
+        "error_list_test.cc",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":error_list",
+        "//aos:json_to_flatbuffer_fbs",
+        "//aos/testing:googletest",
+    ],
+)
+
+cc_library(
     name = "resizeable_buffer",
     hdrs = [
         "resizeable_buffer.h",
diff --git a/aos/containers/error_list.h b/aos/containers/error_list.h
new file mode 100644
index 0000000..2fbd39e
--- /dev/null
+++ b/aos/containers/error_list.h
@@ -0,0 +1,130 @@
+#ifndef AOS_CONTAINERS_ERROR_LIST_H_
+#define AOS_CONTAINERS_ERROR_LIST_H_
+
+#include <iostream>
+
+#include "aos/containers/sized_array.h"
+#include "flatbuffers/flatbuffers.h"
+
+namespace aos {
+
+// A de-duplicated sorted array based on SizedArray
+// For keeping a list of errors that a subsystem has thrown
+// to publish them in a Status message.
+// It is designed to use flatbuffer enums, and use the reserved fields MAX and
+// MIN to automatically determine how much capacity it needs to have.
+template <typename T>
+class ErrorList {
+ private:
+  using array = SizedArray<T, static_cast<size_t>(T::MAX) -
+                                  static_cast<size_t>(T::MIN) + 1>;
+  array array_;
+
+ public:
+  using value_type = typename array::value_type;
+  using size_type = typename array::size_type;
+  using difference_type = typename array::difference_type;
+  using reference = typename array::reference;
+  using const_reference = typename array::const_reference;
+  using pointer = typename array::pointer;
+  using const_pointer = typename array::const_pointer;
+  using iterator = typename array::iterator;
+  using const_iterator = typename array::const_iterator;
+  using reverse_iterator = typename array::reverse_iterator;
+  using const_reverse_iterator = typename array::const_reverse_iterator;
+
+  constexpr ErrorList() = default;
+  ErrorList(const ErrorList &) = default;
+  ErrorList(ErrorList &&) = default;
+  ErrorList(const flatbuffers::Vector<T> &array) : array_() {
+    for (auto it = array.begin(); it < array.end(); it++) {
+      array_.push_back(*it);
+    }
+    std::sort(array_.begin(), array_.end());
+  };
+
+  ErrorList &operator=(const ErrorList &) = default;
+  ErrorList &operator=(ErrorList &&) = default;
+
+  bool operator==(const ErrorList &other) const {
+    if (other.size() != size()) {
+      return false;
+    }
+    for (size_t i = 0; i < size(); ++i) {
+      if (other[i] != (*this)[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+  bool operator!=(const ErrorList &other) const { return !(*this == other); }
+
+  reference at(size_t i) { return array_.at(i); }
+  const_reference at(size_t i) const { return array_.at(i); }
+
+  reference operator[](size_t i) { return array_[i]; }
+  const_reference operator[](size_t i) const { return array_[i]; }
+
+  reference front() { return array_.front(); }
+  const_reference front() const { return array_.front(); }
+
+  reference back() { return array_.back(); }
+  const_reference back() const { return array_.back(); }
+
+  T *data() { return array_.data(); }
+  const T *data() const { return array_.data(); }
+
+  iterator begin() { return array_.begin(); }
+  const_iterator begin() const { return array_.begin(); }
+  const_iterator cbegin() const { return array_.cbegin(); }
+
+  iterator end() { return array_.end(); }
+  const_iterator end() const { return array_.end(); }
+  const_iterator cend() const { return array_.cend(); }
+
+  reverse_iterator rbegin() { return array_.rbegin(); }
+  const_reverse_iterator rbegin() const { return array_.rbegin(); }
+  const_reverse_iterator crbegin() const { return array_.crbegin(); }
+
+  reverse_iterator rend() { return array_.rend(); }
+  const_reverse_iterator rend() const { return array_.rend(); }
+  const_reverse_iterator crend() const { return array_.crend(); }
+
+  bool empty() const { return array_.empty(); }
+  bool full() const { return array_.full(); }
+
+  size_t size() const { return array_.size(); }
+  constexpr size_t max_size() const { return array_.max_size(); }
+
+  void Clear(const T t) {
+    iterator index = std::find(array_.begin(), array_.end(), t);
+    if (index != array_.end()) {
+      array_.erase(index);
+    }
+  }
+
+  void Set(const T t) {
+    iterator position = std::lower_bound(array_.begin(), array_.end(), t);
+
+    // if it found something, and that something is the same, just leave it
+    if (position != array_.end() && *position == t) {
+      return;
+    }
+
+    // key doesn't already exist
+    array_.insert(position, t);
+  }
+
+  bool Has(const T t) {
+    return std::binary_search(array_.begin(), array_.end(), t);
+  }
+
+  flatbuffers::Offset<flatbuffers::Vector<T>> ToFlatbuffer(
+      flatbuffers::FlatBufferBuilder *fbb) const {
+    return fbb->CreateVector(array_.data(), array_.size());
+  }
+};  // namespace aos
+
+}  // namespace aos
+
+#endif  // AOS_CONTAINERS_ERROR_LIST_H_
diff --git a/aos/containers/error_list_test.cc b/aos/containers/error_list_test.cc
new file mode 100644
index 0000000..3ce23c4
--- /dev/null
+++ b/aos/containers/error_list_test.cc
@@ -0,0 +1,115 @@
+#include "aos/containers/error_list.h"
+
+#include "aos/json_to_flatbuffer_generated.h"
+#include "gtest/gtest.h"
+
+namespace aos {
+namespace testing {
+
+enum class TestEnum : int8_t {
+  FOO = 0,
+  BAR = 1,
+  BAZ = 2,
+  VWEEP = 3,
+  MIN = FOO,
+  MAX = VWEEP
+};
+
+// Tests that setting works and allows no duplicates
+TEST(ErrorListTest, NoDuplicates) {
+  ErrorList<TestEnum> a;
+  EXPECT_EQ(a.size(), 0);
+  a.Set(TestEnum::BAZ);
+  EXPECT_EQ(a.at(0), TestEnum::BAZ);
+  EXPECT_EQ(a.size(), 1);
+  a.Set(TestEnum::BAZ);
+  EXPECT_EQ(a.at(0), TestEnum::BAZ);
+  EXPECT_EQ(a.size(), 1);
+  a.Set(TestEnum::VWEEP);
+  EXPECT_EQ(a.at(0), TestEnum::BAZ);
+  EXPECT_EQ(a.at(1), TestEnum::VWEEP);
+  EXPECT_EQ(a.size(), 2);
+  a.Set(TestEnum::FOO);
+  EXPECT_EQ(a.at(0), TestEnum::FOO);
+  EXPECT_EQ(a.at(1), TestEnum::BAZ);
+  EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+  EXPECT_EQ(a.size(), 3);
+}
+
+// Tests that clearing works
+TEST(ErrorListTest, Clearing) {
+  ErrorList<TestEnum> a;
+  a.Set(TestEnum::FOO);
+  a.Set(TestEnum::BAZ);
+  a.Set(TestEnum::VWEEP);
+  EXPECT_EQ(a.at(0), TestEnum::FOO);
+  EXPECT_EQ(a.at(1), TestEnum::BAZ);
+  EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+  EXPECT_EQ(a.size(), 3);
+
+  a.Clear(TestEnum::BAR);
+  EXPECT_EQ(a.at(0), TestEnum::FOO);
+  EXPECT_EQ(a.at(1), TestEnum::BAZ);
+  EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+  EXPECT_EQ(a.size(), 3);
+
+  a.Clear(TestEnum::BAZ);
+  EXPECT_EQ(a.at(0), TestEnum::FOO);
+  EXPECT_EQ(a.at(1), TestEnum::VWEEP);
+  EXPECT_EQ(a.size(), 2);
+}
+
+// Tests that checking for a value works
+TEST(ErrorListTest, Has) {
+  ErrorList<TestEnum> a;
+  a.Set(TestEnum::FOO);
+  a.Set(TestEnum::BAZ);
+  a.Set(TestEnum::VWEEP);
+  EXPECT_EQ(a.at(0), TestEnum::FOO);
+  EXPECT_EQ(a.at(1), TestEnum::BAZ);
+  EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+  EXPECT_EQ(a.size(), 3);
+
+  EXPECT_TRUE(a.Has(TestEnum::FOO));
+  EXPECT_TRUE(a.Has(TestEnum::VWEEP));
+  EXPECT_TRUE(a.Has(TestEnum::BAZ));
+  EXPECT_FALSE(a.Has(TestEnum::BAR));
+}
+
+// Tests serializing and deserializing to/from flatbuffers.
+TEST(ErrorListTest, Flatbuffers) {
+  ErrorList<BaseType> a;
+  a.Set(BaseType::Bool);
+  a.Set(BaseType::Float);
+  a.Set(BaseType::Short);
+  EXPECT_TRUE(a.Has(BaseType::Bool));
+  EXPECT_TRUE(a.Has(BaseType::Short));
+  EXPECT_TRUE(a.Has(BaseType::Float));
+  EXPECT_EQ(a.at(0), BaseType::Bool);
+  EXPECT_EQ(a.at(1), BaseType::Short);
+  EXPECT_EQ(a.at(2), BaseType::Float);
+  EXPECT_EQ(a.size(), 3);
+
+  flatbuffers::FlatBufferBuilder fbb(1024);
+  flatbuffers::Offset<flatbuffers::Vector<BaseType>> vector =
+      a.ToFlatbuffer(&fbb);
+
+  ConfigurationBuilder builder(fbb);
+  builder.add_vector_foo_enum(vector);
+
+  fbb.Finish(builder.Finish());
+  const Configuration *config =
+      flatbuffers::GetRoot<Configuration>(fbb.GetBufferPointer());
+
+  ErrorList<BaseType> b(*config->vector_foo_enum());
+  EXPECT_TRUE(b.Has(BaseType::Bool));
+  EXPECT_TRUE(b.Has(BaseType::Short));
+  EXPECT_TRUE(b.Has(BaseType::Float));
+  EXPECT_EQ(b.at(0), BaseType::Bool);
+  EXPECT_EQ(b.at(1), BaseType::Short);
+  EXPECT_EQ(b.at(2), BaseType::Float);
+  EXPECT_EQ(b.size(), 3);
+}
+
+}  // namespace testing
+}  // namespace aos
diff --git a/aos/containers/sized_array_test.cc b/aos/containers/sized_array_test.cc
index ae732bc..d055f40 100644
--- a/aos/containers/sized_array_test.cc
+++ b/aos/containers/sized_array_test.cc
@@ -175,5 +175,70 @@
   EXPECT_DEATH(a.emplace_back(5), "Aborted at");
 }
 
+// Tests inserting at various positions in the array.
+TEST(SizedArrayTest, Inserting) {
+  SizedArray<int, 5> a;
+  a.insert(a.begin(), 2);
+  EXPECT_EQ(a.at(0), 2);
+  EXPECT_EQ(a.size(), 1);
+
+  a.emplace_back(3);
+  EXPECT_EQ(a.at(0), 2);
+  EXPECT_EQ(a.at(1), 3);
+  EXPECT_EQ(a.size(), 2);
+
+  a.insert(a.begin(), 0);
+  EXPECT_EQ(a.at(0), 0);
+  EXPECT_EQ(a.at(1), 2);
+  EXPECT_EQ(a.at(2), 3);
+  EXPECT_EQ(a.size(), 3);
+
+  a.insert(a.begin() + 1, 1);
+  EXPECT_EQ(a.at(0), 0);
+  EXPECT_EQ(a.at(1), 1);
+  EXPECT_EQ(a.at(2), 2);
+  EXPECT_EQ(a.at(3), 3);
+  EXPECT_EQ(a.size(), 4);
+
+  a.insert(a.begin() + 1, 0);
+  EXPECT_EQ(a.at(0), 0);
+  EXPECT_EQ(a.at(1), 0);
+  EXPECT_EQ(a.at(2), 1);
+  EXPECT_EQ(a.at(3), 2);
+  EXPECT_EQ(a.at(4), 3);
+  EXPECT_EQ(a.size(), 5);
+}
+
+// Tests erasing things from the array
+TEST(SizedArrayTest, Erasing) {
+  SizedArray<int, 5> a;
+  a.push_back(8);
+  a.push_back(9);
+  a.push_back(7);
+  a.push_back(1);
+  a.push_back(5);
+  EXPECT_EQ(a.at(0), 8);
+  EXPECT_EQ(a.at(1), 9);
+  EXPECT_EQ(a.at(2), 7);
+  EXPECT_EQ(a.at(3), 1);
+  EXPECT_EQ(a.at(4), 5);
+  EXPECT_EQ(a.size(), 5);
+
+  a.erase(a.begin() + 1, a.begin() + 3);
+  EXPECT_EQ(a.at(0), 8);
+  EXPECT_EQ(a.at(1), 1);
+  EXPECT_EQ(a.at(2), 5);
+  EXPECT_EQ(a.size(), 3);
+
+  a.erase(a.begin());
+  EXPECT_EQ(a.at(0), 1);
+  EXPECT_EQ(a.at(1), 5);
+  EXPECT_EQ(a.size(), 2);
+
+  a.erase(a.end() - 1);
+  EXPECT_EQ(a.at(0), 1);
+  EXPECT_EQ(a.size(), 1);
+}
+
 }  // namespace testing
 }  // namespace aos
diff --git a/aos/util/foxglove_websocket.cc b/aos/util/foxglove_websocket.cc
index 715ba13..77dca20 100644
--- a/aos/util/foxglove_websocket.cc
+++ b/aos/util/foxglove_websocket.cc
@@ -11,6 +11,10 @@
             "with a read_method of PIN (see aos/configuration.fbs; PIN is an "
             "enum value). Having this enabled will cause foxglove to  consume "
             "extra shared memory resources.");
+DEFINE_bool(
+    canonical_channel_names, false,
+    "If set, use full channel names; by default, will shorten names to be the "
+    "shortest possible version of the name (e.g., /aos instead of /pi/aos).");
 
 int main(int argc, char *argv[]) {
   gflags::SetUsageMessage(
@@ -53,7 +57,10 @@
           : aos::FoxgloveWebsocketServer::Serialization::kJson,
       FLAGS_fetch_pinned_channels
           ? aos::FoxgloveWebsocketServer::FetchPinnedChannels::kYes
-          : aos::FoxgloveWebsocketServer::FetchPinnedChannels::kNo);
+          : aos::FoxgloveWebsocketServer::FetchPinnedChannels::kNo,
+      FLAGS_canonical_channel_names
+          ? aos::FoxgloveWebsocketServer::CanonicalChannelNames::kCanonical
+          : aos::FoxgloveWebsocketServer::CanonicalChannelNames::kShortened);
 
   event_loop.Run();
 }
diff --git a/aos/util/foxglove_websocket_lib.cc b/aos/util/foxglove_websocket_lib.cc
index 1cc3a8a..06c551e 100644
--- a/aos/util/foxglove_websocket_lib.cc
+++ b/aos/util/foxglove_websocket_lib.cc
@@ -1,8 +1,8 @@
 #include "aos/util/foxglove_websocket_lib.h"
 
-#include "aos/util/mcap_logger.h"
-#include "aos/flatbuffer_merge.h"
 #include "absl/strings/escaping.h"
+#include "aos/flatbuffer_merge.h"
+#include "aos/util/mcap_logger.h"
 #include "gflags/gflags.h"
 
 DEFINE_uint32(sorting_buffer_ms, 100,
@@ -17,10 +17,12 @@
 namespace aos {
 FoxgloveWebsocketServer::FoxgloveWebsocketServer(
     aos::EventLoop *event_loop, uint32_t port, Serialization serialization,
-    FetchPinnedChannels fetch_pinned_channels)
+    FetchPinnedChannels fetch_pinned_channels,
+    CanonicalChannelNames canonical_channels)
     : event_loop_(event_loop),
       serialization_(serialization),
       fetch_pinned_channels_(fetch_pinned_channels),
+      canonical_channels_(canonical_channels),
       server_(port, "aos_foxglove") {
   for (const aos::Channel *channel :
        *event_loop_->configuration()->channels()) {
@@ -30,18 +32,28 @@
         (!is_pinned || fetch_pinned_channels_ == FetchPinnedChannels::kYes)) {
       const FlatbufferDetachedBuffer<reflection::Schema> schema =
           RecursiveCopyFlatBuffer(channel->schema());
+      const std::string shortest_name =
+          ShortenedChannelName(event_loop_->configuration(), channel,
+                               event_loop_->name(), event_loop_->node());
+      std::string name_to_send;
+      switch (canonical_channels_) {
+        case CanonicalChannelNames::kCanonical:
+          name_to_send = channel->name()->string_view();
+          break;
+        case CanonicalChannelNames::kShortened:
+          name_to_send = shortest_name;
+          break;
+      }
       const ChannelId id =
           (serialization_ == Serialization::kJson)
               ? server_.addChannel(foxglove::websocket::ChannelWithoutId{
-                    .topic =
-                        channel->name()->str() + " " + channel->type()->str(),
+                    .topic = name_to_send + " " + channel->type()->str(),
                     .encoding = "json",
                     .schemaName = channel->type()->str(),
                     .schema =
                         JsonSchemaForFlatbuffer({channel->schema()}).dump()})
               : server_.addChannel(foxglove::websocket::ChannelWithoutId{
-                    .topic =
-                        channel->name()->str() + " " + channel->type()->str(),
+                    .topic = name_to_send + " " + channel->type()->str(),
                     .encoding = "flatbuffer",
                     .schemaName = channel->type()->str(),
                     .schema = absl::Base64Escape(
diff --git a/aos/util/foxglove_websocket_lib.h b/aos/util/foxglove_websocket_lib.h
index 9be2f61..7c326c2 100644
--- a/aos/util/foxglove_websocket_lib.h
+++ b/aos/util/foxglove_websocket_lib.h
@@ -24,9 +24,20 @@
     kYes,
     kNo,
   };
+  // Whether to attempt to shorten channel names.
+  enum class CanonicalChannelNames {
+    // Just use the full, unambiguous, channel names.
+    kCanonical,
+    // Use GetChannelAliases() to determine the shortest possible name for the
+    // channel for the current node, and use that in the MCAP file. This makes
+    // it so that the channels in the resulting file are more likely to match
+    // the channel names that are used in "real" applications.
+    kShortened,
+  };
   FoxgloveWebsocketServer(aos::EventLoop *event_loop, uint32_t port,
                           Serialization serialization,
-                          FetchPinnedChannels fetch_pinned_channels);
+                          FetchPinnedChannels fetch_pinned_channels,
+                          CanonicalChannelNames canonical_channels);
   ~FoxgloveWebsocketServer();
 
  private:
@@ -47,6 +58,7 @@
   aos::EventLoop *event_loop_;
   const Serialization serialization_;
   const FetchPinnedChannels fetch_pinned_channels_;
+  const CanonicalChannelNames canonical_channels_;
   foxglove::websocket::Server server_;
   // A map of fetchers for every single channel that could be subscribed to.
   std::map<ChannelId, FetcherState> fetchers_;
diff --git a/aos/util/mcap_logger.cc b/aos/util/mcap_logger.cc
index 40e55f0..111d784 100644
--- a/aos/util/mcap_logger.cc
+++ b/aos/util/mcap_logger.cc
@@ -84,6 +84,21 @@
   return schema;
 }
 
+std::string ShortenedChannelName(const aos::Configuration *config,
+                                 const aos::Channel *channel,
+                                 std::string_view application_name,
+                                 const aos::Node *node) {
+  std::set<std::string> names =
+      configuration::GetChannelAliases(config, channel, application_name, node);
+  std::string_view shortest_name;
+  for (const std::string &name : names) {
+    if (shortest_name.empty() || name.size() < shortest_name.size()) {
+      shortest_name = name;
+    }
+  }
+  return std::string(shortest_name);
+}
+
 namespace {
 std::string_view CompressionName(McapLogger::Compression compression) {
   switch (compression) {
@@ -354,15 +369,9 @@
                                   channel->type()->string_view());
         break;
       case CanonicalChannelNames::kShortened: {
-        std::set<std::string> names = configuration::GetChannelAliases(
-            event_loop_->configuration(), channel, event_loop_->name(),
-            event_loop_->node());
-        std::string_view shortest_name;
-        for (const std::string &name : names) {
-          if (shortest_name.empty() || name.size() < shortest_name.size()) {
-            shortest_name = name;
-          }
-        }
+        const std::string shortest_name =
+            ShortenedChannelName(event_loop_->configuration(), channel,
+                                 event_loop_->name(), event_loop_->node());
         if (shortest_name != channel->name()->string_view()) {
           VLOG(1) << "Shortening " << channel->name()->string_view() << " "
                   << channel->type()->string_view() << " to " << shortest_name;
diff --git a/aos/util/mcap_logger.h b/aos/util/mcap_logger.h
index c3fdda1..50e9300 100644
--- a/aos/util/mcap_logger.h
+++ b/aos/util/mcap_logger.h
@@ -24,6 +24,13 @@
     const FlatbufferType &type,
     JsonSchemaRecursion recursion_level = JsonSchemaRecursion::kTopLevel);
 
+// Returns the shortest possible alias for the specified channel on the
+// specified node/application.
+std::string ShortenedChannelName(const aos::Configuration *config,
+                                 const aos::Channel *channel,
+                                 std::string_view application_name,
+                                 const aos::Node *node);
+
 // Generates an MCAP file, per the specification at
 // https://github.com/foxglove/mcap/tree/main/docs/specification
 // This currently generates an uncompressed logfile with full message indexing
diff --git a/aos/util/mcap_logger_test.cc b/aos/util/mcap_logger_test.cc
index 8bc3419..c6febf9 100644
--- a/aos/util/mcap_logger_test.cc
+++ b/aos/util/mcap_logger_test.cc
@@ -10,7 +10,8 @@
 // will require writing an MCAP reader (or importing an existing one).
 
 // Confirm that the schema for the reflection.Schema table itself hasn't
-// changed. reflection.Schema should be a very stable type, so this should need
+// changed. reflection.Schema should be a very stable type, so this should
+// need
 // updating except when we change the JSON schema generation itself.
 TEST(JsonSchemaTest, ReflectionSchema) {
   std::string schema_json =
diff --git a/scouting/deploy/BUILD b/scouting/deploy/BUILD
index ed4b9cd..2bf4b4e 100644
--- a/scouting/deploy/BUILD
+++ b/scouting/deploy/BUILD
@@ -17,6 +17,15 @@
     include_runfiles = True,
     package_dir = "opt/frc971/scouting_server",
     strip_prefix = ".",
+    # The "include_runfiles" attribute creates a runfiles tree as seen from
+    # within the workspace directory. But what we really want is the runfiles
+    # tree as seen from the root of the runfiles tree (i.e. one directory up).
+    # So we work around it by manually adding some symlinks that let us pretend
+    # that we're at the root of the runfiles tree.
+    symlinks = {
+        "opt/frc971/scouting_server/org_frc971": ".",
+        "opt/frc971/scouting_server/bazel_tools": "external/bazel_tools",
+    },
 )
 
 pkg_tar(
diff --git a/scouting/deploy/scouting.service b/scouting/deploy/scouting.service
index b22a57d..5aa64b0 100644
--- a/scouting/deploy/scouting.service
+++ b/scouting/deploy/scouting.service
@@ -7,6 +7,7 @@
 Group=www-data
 Type=simple
 WorkingDirectory=/opt/frc971/scouting_server
+Environment=RUNFILES_DIR=/opt/frc971/scouting_server
 ExecStart=/opt/frc971/scouting_server/scouting/scouting \
     -port 8080 \
     -db_config /var/frc971/scouting/db_config.json \
diff --git a/scouting/www/index.html b/scouting/www/index.html
index 208141a..afc589e 100644
--- a/scouting/www/index.html
+++ b/scouting/www/index.html
@@ -5,16 +5,16 @@
     <title>FRC971 Scouting Application</title>
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <link
-      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
       rel="stylesheet"
-      integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
+      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
+      integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
       crossorigin="anonymous"
     />
     <link
       rel="stylesheet"
-      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"
+      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.c"
     />
-    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
   </head>
   <body>
     <my-app></my-app>