From c6c218970c680126685fd48866c298d5ea28c5a1 Mon Sep 17 00:00:00 2001 From: Schuwi Date: Sat, 20 Sep 2025 11:52:43 +0200 Subject: [PATCH] style: format codebase --- config/dev.exs | 1 + lib/components_elixir/apriltag.ex | 24 +- lib/components_elixir/apriltag/tag36h11.ex | 84 +++-- lib/components_elixir/datasheet_downloader.ex | 38 ++- lib/components_elixir/inventory.ex | 40 ++- lib/components_elixir/inventory/category.ex | 2 +- lib/components_elixir/inventory/component.ex | 13 +- .../inventory/hierarchical.ex | 54 +++- .../inventory/storage_location.ex | 19 +- .../components/core_components.ex | 16 +- .../controllers/file_controller.ex | 12 +- .../live/categories_live.ex | 137 ++++---- .../live/components_live.ex | 81 +++-- lib/components_elixir_web/live/login_live.ex | 2 +- .../live/storage_locations_live.ex | 302 +++++++++++------- lib/mix/tasks/apriltag.generate_all.ex | 6 +- .../20250914160354_migrate_qr_to_apriltag.exs | 4 +- priv/repo/seeds.exs | 265 ++++++++++----- .../controllers/error_html_test.exs | 3 +- .../controllers/error_json_test.exs | 4 +- 20 files changed, 722 insertions(+), 385 deletions(-) diff --git a/config/dev.exs b/config/dev.exs index 263019c..c932b91 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -16,6 +16,7 @@ config :components_elixir, ComponentsElixir.Repo, # For development, use a local uploads directory config :components_elixir, uploads_dir: "./uploads" + # # The watchers configuration can be used to run external # watchers to your application. For example, we can use it diff --git a/lib/components_elixir/apriltag.ex b/lib/components_elixir/apriltag.ex index ef4a110..9ab25e1 100644 --- a/lib/components_elixir/apriltag.ex +++ b/lib/components_elixir/apriltag.ex @@ -31,6 +31,7 @@ defmodule ComponentsElixir.AprilTag do def valid_apriltag_id?(id) when is_integer(id) do id >= 0 and id < @tag36h11_count end + def valid_apriltag_id?(_), do: false @doc """ @@ -78,8 +79,8 @@ defmodule ComponentsElixir.AprilTag do def used_apriltag_ids do ComponentsElixir.Repo.all( from sl in ComponentsElixir.Inventory.StorageLocation, - where: not is_nil(sl.apriltag_id), - select: sl.apriltag_id + where: not is_nil(sl.apriltag_id), + select: sl.apriltag_id ) end @@ -130,10 +131,11 @@ defmodule ComponentsElixir.AprilTag do This should be run once during setup to pre-generate all AprilTag images. """ def generate_all_apriltag_svgs(opts \\ []) do - static_dir = Path.join([ - Application.app_dir(:components_elixir, "priv/static"), - "apriltags" - ]) + static_dir = + Path.join([ + Application.app_dir(:components_elixir, "priv/static"), + "apriltags" + ]) # Ensure directory exists File.mkdir_p!(static_dir) @@ -187,10 +189,12 @@ defmodule ComponentsElixir.AprilTag do """ def cleanup_apriltag_svg(apriltag_id) do filename = "tag36h11_id_#{String.pad_leading(to_string(apriltag_id), 3, "0")}.svg" - file_path = Path.join([ - Application.app_dir(:components_elixir, "priv/static/apriltags"), - filename - ]) + + file_path = + Path.join([ + Application.app_dir(:components_elixir, "priv/static/apriltags"), + filename + ]) if File.exists?(file_path) do File.rm(file_path) diff --git a/lib/components_elixir/apriltag/tag36h11.ex b/lib/components_elixir/apriltag/tag36h11.ex index d73fa64..8ecbbb3 100644 --- a/lib/components_elixir/apriltag/tag36h11.ex +++ b/lib/components_elixir/apriltag/tag36h11.ex @@ -16,6 +16,7 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do case Regex.run(~r/\(april\.tag\.Tag36h11, id = (\d+)\) <([0-9a-fA-F]+)> maketag/, line) do [_, id_str, hex_pattern] -> {String.to_integer(id_str), String.downcase(hex_pattern)} + _ -> nil end @@ -23,26 +24,30 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do # Extract patterns from PostScript file at compile time @all_patterns ( - path = Path.join([File.cwd!(), "apriltags.ps"]) + path = Path.join([File.cwd!(), "apriltags.ps"]) - if File.exists?(path) do - File.read!(path) - |> String.split("\n") - |> Enum.filter(&String.contains?(&1, "april.tag.Tag36h11")) - |> Enum.map(fn line -> - case Regex.run(~r/\(april\.tag\.Tag36h11, id = (\d+)\) <([0-9a-fA-F]+)> maketag/, line) do - [_, id_str, hex_pattern] -> - {String.to_integer(id_str), String.downcase(hex_pattern)} - _ -> - nil - end - end) - |> Enum.reject(&is_nil/1) - |> Map.new() - else - raise "Error: apriltags.ps file not found in project root. This file is required for AprilTag pattern generation." - end - ) + if File.exists?(path) do + File.read!(path) + |> String.split("\n") + |> Enum.filter(&String.contains?(&1, "april.tag.Tag36h11")) + |> Enum.map(fn line -> + case Regex.run( + ~r/\(april\.tag\.Tag36h11, id = (\d+)\) <([0-9a-fA-F]+)> maketag/, + line + ) do + [_, id_str, hex_pattern] -> + {String.to_integer(id_str), String.downcase(hex_pattern)} + + _ -> + nil + end + end) + |> Enum.reject(&is_nil/1) + |> Map.new() + else + raise "Error: apriltags.ps file not found in project root. This file is required for AprilTag pattern generation." + end + ) # Sample of real tag36h11 hex patterns from AprilRobotics repository # This will be populated with patterns extracted from the PostScript file @@ -64,7 +69,8 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do |> Enum.reject(&is_nil/1) |> Map.new() else - %{} # Return empty map if file not found, will fall back to hardcoded patterns + # Return empty map if file not found, will fall back to hardcoded patterns + %{} end end @@ -76,6 +82,7 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do def get_hex_pattern(id) when is_integer(id) and id >= 0 and id < 587 do Map.get(@tag36h11_patterns, id) end + def get_hex_pattern(_), do: nil @doc """ @@ -97,6 +104,7 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do # Each scanline is padded to a byte boundary: 3 bytes per 10 pixels (2 bpp) row_bytes = 3 + rows = for row <- 0..9 do <<_::binary-size(row * row_bytes), r::binary-size(row_bytes), _::binary>> = bytes @@ -104,12 +112,23 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do samples = [ - b0 >>> 6 &&& 0x3, b0 >>> 4 &&& 0x3, b0 >>> 2 &&& 0x3, b0 &&& 0x3, - b1 >>> 6 &&& 0x3, b1 >>> 4 &&& 0x3, b1 >>> 2 &&& 0x3, b1 &&& 0x3, - b2 >>> 6 &&& 0x3, b2 >>> 4 &&& 0x3, b2 >>> 2 &&& 0x3, b2 &&& 0x3 + b0 >>> 6 &&& 0x3, + b0 >>> 4 &&& 0x3, + b0 >>> 2 &&& 0x3, + b0 &&& 0x3, + b1 >>> 6 &&& 0x3, + b1 >>> 4 &&& 0x3, + b1 >>> 2 &&& 0x3, + b1 &&& 0x3, + b2 >>> 6 &&& 0x3, + b2 >>> 4 &&& 0x3, + b2 >>> 2 &&& 0x3, + b2 &&& 0x3 ] - |> Enum.take(10) # drop the 2 padding samples at end of row - |> Enum.map(&(&1 == 0)) # 0 = black, 3 = white → boolean + # drop the 2 padding samples at end of row + |> Enum.take(10) + # 0 = black, 3 = white → boolean + |> Enum.map(&(&1 == 0)) samples end @@ -133,7 +152,8 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do Only black modules are drawn over a white background. """ def binary_matrix_to_svg(binary_matrix, opts \\ []) do - size = Keyword.get(opts, :size, 200) # final CSS size in px + # final CSS size in px + size = Keyword.get(opts, :size, 200) id_text = Keyword.get(opts, :id_text, "") # binary_matrix is 10x10 of booleans: true=black, false=white @@ -172,9 +192,11 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do #{if id_text != "" do - ~s(#{id_text}) - else "" end} + else + "" + end} """ end @@ -197,11 +219,13 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do opts_with_id = Keyword.put(opts, :id_text, id_text) binary_matrix_to_svg(binary_matrix, opts_with_id) end - end # Generate a placeholder pattern for IDs we don't have real data for yet + end + + # Generate a placeholder pattern for IDs we don't have real data for yet defp generate_placeholder_svg(id, opts) do size = Keyword.get(opts, :size, 200) margin = Keyword.get(opts, :margin, div(size, 10)) - square_size = size - (2 * margin) + square_size = size - 2 * margin """ diff --git a/lib/components_elixir/datasheet_downloader.ex b/lib/components_elixir/datasheet_downloader.ex index e8368d6..e8b7b00 100644 --- a/lib/components_elixir/datasheet_downloader.ex +++ b/lib/components_elixir/datasheet_downloader.ex @@ -28,6 +28,7 @@ defmodule ComponentsElixir.DatasheetDownloader do case URI.parse(url) do %URI{scheme: scheme} when scheme in ["http", "https"] -> {:ok, URI.parse(url)} + _ -> {:error, "Invalid URL scheme. Only HTTP and HTTPS are supported."} end @@ -36,9 +37,12 @@ defmodule ComponentsElixir.DatasheetDownloader do defp generate_filename(url) do # Try to extract a meaningful filename from the URL uri = URI.parse(url) + original_filename = case Path.basename(uri.path || "") do - "" -> "datasheet" + "" -> + "datasheet" + basename -> # Remove extension and sanitize basename @@ -54,10 +58,14 @@ defmodule ComponentsElixir.DatasheetDownloader do defp sanitize_filename(filename) do filename - |> String.replace(~r/[^\w\-_]/, "_") # Replace non-word chars with underscores - |> String.replace(~r/_+/, "_") # Replace multiple underscores with single - |> String.trim("_") # Remove leading/trailing underscores - |> String.slice(0, 50) # Limit length + # Replace non-word chars with underscores + |> String.replace(~r/[^\w\-_]/, "_") + # Replace multiple underscores with single + |> String.replace(~r/_+/, "_") + # Remove leading/trailing underscores + |> String.trim("_") + # Limit length + |> String.slice(0, 50) |> case do "" -> "datasheet" name -> name @@ -66,17 +74,19 @@ defmodule ComponentsElixir.DatasheetDownloader do defp fetch_pdf(url) do case Req.get(url, - redirect: true, - max_redirects: 5, - receive_timeout: 30_000, - headers: [ - {"User-Agent", "ComponentSystem/1.0 DatasheetDownloader"} - ] - ) do + redirect: true, + max_redirects: 5, + receive_timeout: 30_000, + headers: [ + {"User-Agent", "ComponentSystem/1.0 DatasheetDownloader"} + ] + ) do {:ok, %Req.Response{status: 200} = response} -> {:ok, response} + {:ok, %Req.Response{status: status}} -> {:error, "HTTP error: #{status}"} + {:error, reason} -> Logger.error("Failed to download PDF from #{url}: #{inspect(reason)}") {:error, "Download failed: #{inspect(reason)}"} @@ -88,6 +98,7 @@ defmodule ComponentsElixir.DatasheetDownloader do case body do <<"%PDF", _rest::binary>> -> :ok + _ -> {:error, "Downloaded content is not a valid PDF file"} end @@ -105,10 +116,12 @@ defmodule ComponentsElixir.DatasheetDownloader do :ok -> Logger.info("Successfully saved datasheet: #{filename}") :ok + {:error, reason} -> Logger.error("Failed to save datasheet file: #{inspect(reason)}") {:error, "Failed to save file: #{inspect(reason)}"} end + {:error, reason} -> Logger.error("Failed to create datasheets directory: #{inspect(reason)}") {:error, "Failed to create directory: #{inspect(reason)}"} @@ -129,6 +142,7 @@ defmodule ComponentsElixir.DatasheetDownloader do :ok -> Logger.info("Deleted datasheet file: #{filename}") :ok + {:error, reason} -> Logger.warning("Failed to delete datasheet file #{filename}: #{inspect(reason)}") {:error, reason} diff --git a/lib/components_elixir/inventory.ex b/lib/components_elixir/inventory.ex index 9545fb1..ade789f 100644 --- a/lib/components_elixir/inventory.ex +++ b/lib/components_elixir/inventory.ex @@ -19,7 +19,7 @@ defmodule ComponentsElixir.Inventory do locations = StorageLocation |> order_by([sl], asc: sl.name) - |> preload([parent: [parent: [parent: [parent: :parent]]]]) + |> preload(parent: [parent: [parent: [parent: :parent]]]) |> Repo.all() # Ensure AprilTag SVGs exist for all locations @@ -162,7 +162,7 @@ defmodule ComponentsElixir.Inventory do """ def list_categories do Category - |> preload([parent: [parent: [parent: [parent: :parent]]]]) + |> preload(parent: [parent: [parent: [parent: :parent]]]) |> Repo.all() end @@ -217,8 +217,15 @@ defmodule ComponentsElixir.Inventory do # Verify the category exists before getting descendants case Enum.find(categories, &(&1.id == category_id)) do - nil -> [] - _category -> ComponentsElixir.Inventory.Hierarchical.descendant_ids(categories, category_id, &(&1.parent_id)) + nil -> + [] + + _category -> + ComponentsElixir.Inventory.Hierarchical.descendant_ids( + categories, + category_id, + & &1.parent_id + ) end end @@ -233,13 +240,21 @@ defmodule ComponentsElixir.Inventory do for typical storage location tree sizes (hundreds of locations). For very large storage location trees, a recursive CTE query could be used instead. """ - def get_storage_location_and_descendant_ids(storage_location_id) when is_integer(storage_location_id) do + def get_storage_location_and_descendant_ids(storage_location_id) + when is_integer(storage_location_id) do storage_locations = list_storage_locations() # Verify the storage location exists before getting descendants case Enum.find(storage_locations, &(&1.id == storage_location_id)) do - nil -> [] - _storage_location -> ComponentsElixir.Inventory.Hierarchical.descendant_ids(storage_locations, storage_location_id, &(&1.parent_id)) + nil -> + [] + + _storage_location -> + ComponentsElixir.Inventory.Hierarchical.descendant_ids( + storage_locations, + storage_location_id, + & &1.parent_id + ) end end @@ -306,7 +321,7 @@ defmodule ComponentsElixir.Inventory do } defp get_sort_order(criteria) do - Map.get(@sort_orders, criteria, [asc: :name, asc: :id]) + Map.get(@sort_orders, criteria, asc: :name, asc: :id) end @doc """ @@ -338,10 +353,12 @@ defmodule ComponentsElixir.Inventory do case ComponentsElixir.DatasheetDownloader.download_pdf_from_url(url) do {:ok, filename} -> Map.put(attrs, "datasheet_filename", filename) + {:error, _reason} -> # Continue without datasheet file if download fails attrs end + _ -> attrs end @@ -372,13 +389,18 @@ defmodule ComponentsElixir.Inventory do {:ok, filename} -> # Delete old datasheet file if it exists if component.datasheet_filename do - ComponentsElixir.DatasheetDownloader.delete_datasheet_file(component.datasheet_filename) + ComponentsElixir.DatasheetDownloader.delete_datasheet_file( + component.datasheet_filename + ) end + Map.put(attrs, "datasheet_filename", filename) + {:error, _reason} -> # Keep existing filename if download fails attrs end + _ -> attrs end diff --git a/lib/components_elixir/inventory/category.ex b/lib/components_elixir/inventory/category.ex index 06a09e0..4ea8a2d 100644 --- a/lib/components_elixir/inventory/category.ex +++ b/lib/components_elixir/inventory/category.ex @@ -37,7 +37,7 @@ defmodule ComponentsElixir.Inventory.Category do """ @impl true def full_path(%Category{} = category) do - Hierarchical.full_path(category, &(&1.parent), path_separator()) + Hierarchical.full_path(category, & &1.parent, path_separator()) end @impl true diff --git a/lib/components_elixir/inventory/component.ex b/lib/components_elixir/inventory/component.ex index 1c656ba..72435b2 100644 --- a/lib/components_elixir/inventory/component.ex +++ b/lib/components_elixir/inventory/component.ex @@ -30,7 +30,18 @@ defmodule ComponentsElixir.Inventory.Component do @doc false def changeset(component, attrs) do component - |> cast(attrs, [:name, :description, :keywords, :position, :count, :datasheet_url, :datasheet_filename, :image_filename, :category_id, :storage_location_id]) + |> cast(attrs, [ + :name, + :description, + :keywords, + :position, + :count, + :datasheet_url, + :datasheet_filename, + :image_filename, + :category_id, + :storage_location_id + ]) |> validate_required([:name, :category_id]) |> validate_length(:name, min: 1, max: 255) |> validate_length(:description, max: 2000) diff --git a/lib/components_elixir/inventory/hierarchical.ex b/lib/components_elixir/inventory/hierarchical.ex index 02473d9..56c6c4a 100644 --- a/lib/components_elixir/inventory/hierarchical.ex +++ b/lib/components_elixir/inventory/hierarchical.ex @@ -29,10 +29,12 @@ defmodule ComponentsElixir.Inventory.Hierarchical do case parent_accessor_fn.(entity) do nil -> entity.name + %Ecto.Association.NotLoaded{} -> # Parent not loaded - fall back to database lookup # This is a fallback and should be rare if preloading is done correctly build_path_with_db_lookup(entity, separator) + parent -> "#{full_path(parent, parent_accessor_fn, separator)}#{separator}#{entity.name}" end @@ -52,12 +54,14 @@ defmodule ComponentsElixir.Inventory.Hierarchical do nil -> # This is a root entity, add its name and return the complete path [entity.name | path_so_far] + parent_id -> # Load parent from database case load_parent_entity(entity, parent_id) do nil -> # Parent not found (orphaned record), treat this as root [entity.name | path_so_far] + parent -> # Recursively get the path from the parent, then add current entity collect_path_from_root(parent, [entity.name | path_so_far]) @@ -93,9 +97,9 @@ defmodule ComponentsElixir.Inventory.Hierarchical do entity_id = id_accessor_fn.(entity) # Remove self-reference - entity_id == editing_entity_id || # Remove descendants (they would create a cycle) - descendant?(entities, entity_id, editing_entity_id, parent_id_accessor_fn) + entity_id == editing_entity_id || + descendant?(entities, entity_id, editing_entity_id, parent_id_accessor_fn) end) end @@ -114,13 +118,21 @@ defmodule ComponentsElixir.Inventory.Hierarchical do defp descendant_recursive?(entities, entity, ancestor_id, parent_id_accessor_fn) do case parent_id_accessor_fn.(entity) do - nil -> false - ^ancestor_id -> true + nil -> + false + + ^ancestor_id -> + true + parent_id -> parent = Enum.find(entities, fn e -> e.id == parent_id end) + case parent do - nil -> false - parent_entity -> descendant_recursive?(entities, parent_entity, ancestor_id, parent_id_accessor_fn) + nil -> + false + + parent_entity -> + descendant_recursive?(entities, parent_entity, ancestor_id, parent_id_accessor_fn) end end end @@ -182,15 +194,20 @@ defmodule ComponentsElixir.Inventory.Hierarchical do Includes proper filtering to prevent cycles and formatted display names. Results are sorted hierarchically for intuitive navigation. """ - def parent_select_options(entities, editing_entity_id, parent_accessor_fn, nil_option_text \\ "No parent") do + def parent_select_options( + entities, + editing_entity_id, + parent_accessor_fn, + nil_option_text \\ "No parent" + ) do available_entities = filter_parent_options( entities, editing_entity_id, - &(&1.id), - &(&1.parent_id) + & &1.id, + & &1.parent_id ) - |> sort_hierarchically(&(&1.parent_id)) + |> sort_hierarchically(& &1.parent_id) |> Enum.map(fn entity -> {display_name(entity, parent_accessor_fn), entity.id} end) @@ -205,7 +222,7 @@ defmodule ComponentsElixir.Inventory.Hierarchical do def select_options(entities, parent_accessor_fn, nil_option_text \\ nil) do sorted_entities = entities - |> sort_hierarchically(&(&1.parent_id)) + |> sort_hierarchically(& &1.parent_id) |> Enum.map(fn entity -> {display_name(entity, parent_accessor_fn), entity.id} end) @@ -300,9 +317,10 @@ defmodule ComponentsElixir.Inventory.Hierarchical do descendant_ids_only = List.delete(all_descendant_ids, entity_id) # Sum counts for all descendants - children_count = Enum.reduce(descendant_ids_only, 0, fn id, acc -> - acc + count_fn.(id) - end) + children_count = + Enum.reduce(descendant_ids_only, 0, fn id, acc -> + acc + count_fn.(id) + end) {self_count, children_count, self_count + children_count} end @@ -320,7 +338,13 @@ defmodule ComponentsElixir.Inventory.Hierarchical do - singular_noun: What to call a single item (default: "component") - plural_noun: What to call multiple items (default: "components") """ - def format_count_display(self_count, children_count, is_expanded, singular_noun \\ "component", plural_noun \\ "components") do + def format_count_display( + self_count, + children_count, + is_expanded, + singular_noun \\ "component", + plural_noun \\ "components" + ) do total_count = self_count + children_count count_noun = if total_count == 1, do: singular_noun, else: plural_noun diff --git a/lib/components_elixir/inventory/storage_location.ex b/lib/components_elixir/inventory/storage_location.ex index 3421b4f..9d853d8 100644 --- a/lib/components_elixir/inventory/storage_location.ex +++ b/lib/components_elixir/inventory/storage_location.ex @@ -25,7 +25,7 @@ defmodule ComponentsElixir.Inventory.StorageLocation do timestamps(type: :naive_datetime_usec) end - @doc false + @doc false def changeset(storage_location, attrs) do storage_location |> cast(attrs, [:name, :description, :parent_id, :apriltag_id]) @@ -40,7 +40,7 @@ defmodule ComponentsElixir.Inventory.StorageLocation do # HierarchicalSchema implementations @impl true def full_path(%StorageLocation{} = storage_location) do - Hierarchical.full_path(storage_location, &(&1.parent), path_separator()) + Hierarchical.full_path(storage_location, & &1.parent, path_separator()) end @impl true @@ -80,11 +80,12 @@ defmodule ComponentsElixir.Inventory.StorageLocation do defp get_next_available_apriltag_id do # Get all used AprilTag IDs - used_ids = ComponentsElixir.Repo.all( - from sl in ComponentsElixir.Inventory.StorageLocation, - where: not is_nil(sl.apriltag_id), - select: sl.apriltag_id - ) + used_ids = + ComponentsElixir.Repo.all( + from sl in ComponentsElixir.Inventory.StorageLocation, + where: not is_nil(sl.apriltag_id), + select: sl.apriltag_id + ) # Find the first available ID (0-586) 0..586 @@ -93,7 +94,9 @@ defmodule ComponentsElixir.Inventory.StorageLocation do nil -> # All IDs are used - this should be handled at the application level raise "All AprilTag IDs are in use" - id -> id + + id -> + id end end end diff --git a/lib/components_elixir_web/components/core_components.ex b/lib/components_elixir_web/components/core_components.ex index 83a6c00..fa98d40 100644 --- a/lib/components_elixir_web/components/core_components.ex +++ b/lib/components_elixir_web/components/core_components.ex @@ -195,7 +195,10 @@ defmodule ComponentsElixirWeb.CoreComponents do name={@name} value="true" checked={@checked} - class={@class || "checkbox checkbox-sm border-base-300 checked:bg-primary checked:border-primary"} + class={ + @class || + "checkbox checkbox-sm border-base-300 checked:bg-primary checked:border-primary" + } {@rest} />{@label} @@ -213,7 +216,10 @@ defmodule ComponentsElixirWeb.CoreComponents do
0 && "mt-4"]} style={"margin-left: #{@margin_left}px"}> @@ -215,12 +225,16 @@ defmodule ComponentsElixirWeb.CategoriesLive do <% end %> <% else %> -
+
+ <% end %> - <.icon name="hero-folder" class={"#{@icon_size} #{if @depth == 0, do: "text-primary", else: "text-base-content/60"} mt-0.5"} /> - - + <.icon + name="hero-folder" + class={"#{@icon_size} #{if @depth == 0, do: "text-primary", else: "text-base-content/60"} mt-0.5"} + /> + +
<%= unless @is_expanded do %> @@ -268,8 +282,8 @@ defmodule ComponentsElixirWeb.CategoriesLive do
<% end %> - - + + <%= if @is_expanded do %>
@@ -321,11 +335,16 @@ defmodule ComponentsElixirWeb.CategoriesLive do
- - + + <%= if @is_expanded do %> <%= for child <- @children do %> - <.category_item category={child} categories={@categories} expanded_categories={@expanded_categories} depth={@depth + 1} /> + <.category_item + category={child} + categories={@categories} + expanded_categories={@expanded_categories} + depth={@depth + 1} + /> <% end %> <% end %> @@ -368,8 +387,8 @@ defmodule ComponentsElixirWeb.CategoriesLive do - - + + <%= if @show_add_form do %>
@@ -424,8 +443,8 @@ defmodule ComponentsElixirWeb.CategoriesLive do
<% end %> - - + + <%= if @show_edit_form do %>
@@ -480,13 +499,15 @@ defmodule ComponentsElixirWeb.CategoriesLive do
<% end %> - - + +

Category Hierarchy

-

Manage your component categories and subcategories

+

+ Manage your component categories and subcategories +

<%= if Enum.empty?(@categories) do %> @@ -501,8 +522,7 @@ defmodule ComponentsElixirWeb.CategoriesLive do phx-click="show_add_form" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700" > - <.icon name="hero-plus" class="w-4 h-4 mr-2" /> - Add Category + <.icon name="hero-plus" class="w-4 h-4 mr-2" /> Add Category
@@ -511,7 +531,12 @@ defmodule ComponentsElixirWeb.CategoriesLive do <%= for category <- root_categories(@categories) do %>
- <.category_item category={category} categories={@categories} expanded_categories={@expanded_categories} depth={0} /> + <.category_item + category={category} + categories={@categories} + expanded_categories={@expanded_categories} + depth={0} + />
<% end %> diff --git a/lib/components_elixir_web/live/components_live.ex b/lib/components_elixir_web/live/components_live.ex index f18bd5f..67d604f 100644 --- a/lib/components_elixir_web/live/components_live.ex +++ b/lib/components_elixir_web/live/components_live.ex @@ -139,7 +139,11 @@ defmodule ComponentsElixirWeb.ComponentsLive do |> push_patch(to: path)} end - def handle_event("storage_location_filter", %{"storage_location_id" => storage_location_id}, socket) do + def handle_event( + "storage_location_filter", + %{"storage_location_id" => storage_location_id}, + socket + ) do storage_location_id = String.to_integer(storage_location_id) query_string = build_query_params_with_storage_location(socket, storage_location_id) path = if query_string == "", do: "/", else: "/?" <> query_string @@ -387,7 +391,10 @@ defmodule ComponentsElixirWeb.ComponentsLive do |> save_uploaded_image(component_params) |> save_uploaded_datasheet(socket) - case Inventory.update_component_with_datasheet(socket.assigns.editing_component, updated_params) do + case Inventory.update_component_with_datasheet( + socket.assigns.editing_component, + updated_params + ) do {:ok, _component} -> {:noreply, socket @@ -496,7 +503,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do search: Map.get(overrides, :search, socket.assigns.search), criteria: Map.get(overrides, :criteria, socket.assigns.sort_criteria), category_id: Map.get(overrides, :category_id, socket.assigns.selected_category), - storage_location_id: Map.get(overrides, :storage_location_id, socket.assigns.selected_storage_location) + storage_location_id: + Map.get(overrides, :storage_location_id, socket.assigns.selected_storage_location) } params @@ -506,12 +514,14 @@ defmodule ComponentsElixirWeb.ComponentsLive do defp parse_filter_id(nil), do: nil defp parse_filter_id(""), do: nil + defp parse_filter_id(id) when is_binary(id) do case Integer.parse(id) do {int_id, ""} -> int_id _ -> nil end end + defp parse_filter_id(id) when is_integer(id), do: id defp build_query_params_with_category(socket, category_id) do @@ -553,7 +563,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do end defp category_options(categories) do - Hierarchical.select_options(categories, &(&1.parent), "Select a category") + Hierarchical.select_options(categories, & &1.parent, "Select a category") end defp storage_location_display_name(location) do @@ -561,7 +571,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do end defp storage_location_options(storage_locations) do - Hierarchical.select_options(storage_locations, &(&1.parent), "No storage location") + Hierarchical.select_options(storage_locations, & &1.parent, "No storage location") end @impl true @@ -610,7 +620,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do - +
@@ -687,23 +697,27 @@ defmodule ComponentsElixirWeb.ComponentsLive do class="inline-flex items-center px-3 py-2 border border-base-300 text-sm font-medium rounded-md text-base-content bg-base-100 hover:bg-base-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary" > <.icon name="hero-adjustments-horizontal" class="w-4 h-4 mr-2" /> - <%= if @show_advanced_filters, do: "Hide", else: "More" %> Filters + {if @show_advanced_filters, do: "Hide", else: "More"} Filters
- - + + <%= if @show_advanced_filters do %>
- +
- Available IDs: <%= length(@available_apriltag_ids) %> of 587 + Available IDs: {length(@available_apriltag_ids)} of 587 <%= if length(@available_apriltag_ids) < 20 do %> -
Next available: <%= @available_apriltag_ids |> Enum.take(10) |> Enum.join(", ") %> - <%= if length(@available_apriltag_ids) > 10, do: "..." %> +
Next available: {@available_apriltag_ids + |> Enum.take(10) + |> Enum.join(", ")} + {if length(@available_apriltag_ids) > 10, do: "..."} <% end %>
@@ -678,8 +721,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
<% end %> - - + + <%= if @show_edit_form do %>
@@ -773,12 +816,14 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do class="w-32" />
- Available IDs: <%= length(@available_apriltag_ids) %> of 587 + Available IDs: {length(@available_apriltag_ids)} of 587
<% end %>

- Current: <%= if @editing_location.apriltag_id, do: "ID #{@editing_location.apriltag_id}", else: "None" %> + Current: {if @editing_location.apriltag_id, + do: "ID #{@editing_location.apriltag_id}", + else: "None"}

@@ -803,8 +848,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do <% end %> - - + + <%= if @apriltag_scanner_open do %>
@@ -818,16 +863,20 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do <.icon name="hero-x-mark" class="w-6 h-6" />
- - + +
<.icon name="hero-qr-code" class="mx-auto h-12 w-12 text-base-content/50" />

Camera AprilTag scanner would go here

-

In a real implementation, this would use JavaScript AprilTag detection

- - +

+ In a real implementation, this would use JavaScript AprilTag detection +

+ +
-

Test with sample AprilTag IDs:

+

+ Test with sample AprilTag IDs: +

<% end %> - - + + <%= if length(@scanned_tags) > 0 do %>
@@ -863,26 +912,35 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
-
+
- {location_display_name(scan.location)} - (AprilTag ID {scan.apriltag_id}) + + {location_display_name(scan.location)} + + + (AprilTag ID {scan.apriltag_id}) +
- Level <%= Hierarchical.compute_level(scan.location, &(&1.parent)) %> + Level {Hierarchical.compute_level(scan.location, & &1.parent)}
<% end %> - - + +

Storage Location Hierarchy

-

Manage your physical storage locations and AprilTags

+

+ Manage your physical storage locations and AprilTags +

<%= if Enum.empty?(@storage_locations) do %> @@ -897,8 +955,7 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do phx-click="show_add_form" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700" > - <.icon name="hero-plus" class="w-4 h-4 mr-2" /> - Add Location + <.icon name="hero-plus" class="w-4 h-4 mr-2" /> Add Location
@@ -907,7 +964,12 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do <%= for location <- root_storage_locations(@storage_locations) do %>
- <.location_item location={location} storage_locations={@storage_locations} expanded_locations={@expanded_locations} depth={0} /> + <.location_item + location={location} + storage_locations={@storage_locations} + expanded_locations={@expanded_locations} + depth={0} + />
<% end %>
diff --git a/lib/mix/tasks/apriltag.generate_all.ex b/lib/mix/tasks/apriltag.generate_all.ex index e69a7ae..5f0b886 100644 --- a/lib/mix/tasks/apriltag.generate_all.ex +++ b/lib/mix/tasks/apriltag.generate_all.ex @@ -25,9 +25,8 @@ defmodule Mix.Tasks.Apriltag.GenerateAll do start_time = System.monotonic_time(:millisecond) - result = ComponentsElixir.AprilTag.generate_all_apriltag_svgs( - force_regenerate: force_regenerate - ) + result = + ComponentsElixir.AprilTag.generate_all_apriltag_svgs(force_regenerate: force_regenerate) end_time = System.monotonic_time(:millisecond) duration = end_time - start_time @@ -39,6 +38,7 @@ defmodule Mix.Tasks.Apriltag.GenerateAll do if result.errors > 0 do IO.puts("\nErrors encountered:") + result.results |> Enum.filter(&match?({:error, _, _}, &1)) |> Enum.each(fn {:error, id, reason} -> diff --git a/priv/repo/migrations/20250914160354_migrate_qr_to_apriltag.exs b/priv/repo/migrations/20250914160354_migrate_qr_to_apriltag.exs index 3a55848..baa1b2e 100644 --- a/priv/repo/migrations/20250914160354_migrate_qr_to_apriltag.exs +++ b/priv/repo/migrations/20250914160354_migrate_qr_to_apriltag.exs @@ -14,7 +14,9 @@ defmodule ComponentsElixir.Repo.Migrations.MigrateQrToApriltag do create unique_index(:storage_locations, [:apriltag_id]) # Add constraint to ensure apriltag_id is in valid range (0-586 for tag36h11) - create constraint(:storage_locations, :apriltag_id_range, check: "apriltag_id >= 0 AND apriltag_id <= 586") + create constraint(:storage_locations, :apriltag_id_range, + check: "apriltag_id >= 0 AND apriltag_id <= 586" + ) # Note: We keep qr_code_old for now in case we need to rollback # It can be removed in a future migration after confirming everything works diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 81a75a5..269e4c4 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -25,102 +25,208 @@ Repo.delete_all(Category) Repo.delete_all(StorageLocation) # Create categories -{:ok, resistors} = Inventory.create_category(%{name: "Resistors", description: "Various types of resistors"}) -{:ok, capacitors} = Inventory.create_category(%{name: "Capacitors", description: "Electrolytic, ceramic, and film capacitors"}) -{:ok, semiconductors} = Inventory.create_category(%{name: "Semiconductors", description: "ICs, transistors, diodes"}) -{:ok, connectors} = Inventory.create_category(%{name: "Connectors", description: "Headers, terminals, plugs"}) +{:ok, resistors} = + Inventory.create_category(%{name: "Resistors", description: "Various types of resistors"}) + +{:ok, capacitors} = + Inventory.create_category(%{ + name: "Capacitors", + description: "Electrolytic, ceramic, and film capacitors" + }) + +{:ok, semiconductors} = + Inventory.create_category(%{name: "Semiconductors", description: "ICs, transistors, diodes"}) + +{:ok, connectors} = + Inventory.create_category(%{name: "Connectors", description: "Headers, terminals, plugs"}) # Create subcategories -{:ok, _through_hole_resistors} = Inventory.create_category(%{ - name: "Through-hole", - description: "Traditional leaded resistors", - parent_id: resistors.id -}) +{:ok, _through_hole_resistors} = + Inventory.create_category(%{ + name: "Through-hole", + description: "Traditional leaded resistors", + parent_id: resistors.id + }) -{:ok, _smd_resistors} = Inventory.create_category(%{ - name: "SMD/SMT", - description: "Surface mount resistors", - parent_id: resistors.id -}) +{:ok, _smd_resistors} = + Inventory.create_category(%{ + name: "SMD/SMT", + description: "Surface mount resistors", + parent_id: resistors.id + }) -{:ok, _ceramic_caps} = Inventory.create_category(%{ - name: "Ceramic", - description: "Ceramic disc and multilayer capacitors", - parent_id: capacitors.id -}) +{:ok, _ceramic_caps} = + Inventory.create_category(%{ + name: "Ceramic", + description: "Ceramic disc and multilayer capacitors", + parent_id: capacitors.id + }) -{:ok, _electrolytic_caps} = Inventory.create_category(%{ - name: "Electrolytic", - description: "Polarized electrolytic capacitors", - parent_id: capacitors.id -}) +{:ok, _electrolytic_caps} = + Inventory.create_category(%{ + name: "Electrolytic", + description: "Polarized electrolytic capacitors", + parent_id: capacitors.id + }) # Create a DEEP category hierarchy to test fallback path (7+ levels) -{:ok, deep_cat_1} = Inventory.create_category(%{name: "Level 1", description: "Deep hierarchy test", parent_id: resistors.id}) -{:ok, deep_cat_2} = Inventory.create_category(%{name: "Level 2", description: "Deep hierarchy test", parent_id: deep_cat_1.id}) -{:ok, deep_cat_3} = Inventory.create_category(%{name: "Level 3", description: "Deep hierarchy test", parent_id: deep_cat_2.id}) -{:ok, deep_cat_4} = Inventory.create_category(%{name: "Level 4", description: "Deep hierarchy test", parent_id: deep_cat_3.id}) -{:ok, deep_cat_5} = Inventory.create_category(%{name: "Level 5", description: "Deep hierarchy test", parent_id: deep_cat_4.id}) -{:ok, deep_cat_6} = Inventory.create_category(%{name: "Level 6", description: "Deep hierarchy test", parent_id: deep_cat_5.id}) -{:ok, deep_cat_7} = Inventory.create_category(%{name: "Level 7", description: "Deep hierarchy test - triggers fallback", parent_id: deep_cat_6.id}) +{:ok, deep_cat_1} = + Inventory.create_category(%{ + name: "Level 1", + description: "Deep hierarchy test", + parent_id: resistors.id + }) + +{:ok, deep_cat_2} = + Inventory.create_category(%{ + name: "Level 2", + description: "Deep hierarchy test", + parent_id: deep_cat_1.id + }) + +{:ok, deep_cat_3} = + Inventory.create_category(%{ + name: "Level 3", + description: "Deep hierarchy test", + parent_id: deep_cat_2.id + }) + +{:ok, deep_cat_4} = + Inventory.create_category(%{ + name: "Level 4", + description: "Deep hierarchy test", + parent_id: deep_cat_3.id + }) + +{:ok, deep_cat_5} = + Inventory.create_category(%{ + name: "Level 5", + description: "Deep hierarchy test", + parent_id: deep_cat_4.id + }) + +{:ok, deep_cat_6} = + Inventory.create_category(%{ + name: "Level 6", + description: "Deep hierarchy test", + parent_id: deep_cat_5.id + }) + +{:ok, deep_cat_7} = + Inventory.create_category(%{ + name: "Level 7", + description: "Deep hierarchy test - triggers fallback", + parent_id: deep_cat_6.id + }) # Create storage locations -{:ok, shelf_a} = Inventory.create_storage_location(%{name: "Shelf A", description: "Main electronics shelf"}) -{:ok, _shelf_b} = Inventory.create_storage_location(%{name: "Shelf B", description: "Components overflow shelf"}) +{:ok, shelf_a} = + Inventory.create_storage_location(%{name: "Shelf A", description: "Main electronics shelf"}) + +{:ok, _shelf_b} = + Inventory.create_storage_location(%{name: "Shelf B", description: "Components overflow shelf"}) # Create drawers on Shelf A -{:ok, drawer_a1} = Inventory.create_storage_location(%{ - name: "Drawer 1", - description: "Resistors and capacitors", - parent_id: shelf_a.id -}) +{:ok, drawer_a1} = + Inventory.create_storage_location(%{ + name: "Drawer 1", + description: "Resistors and capacitors", + parent_id: shelf_a.id + }) -{:ok, drawer_a2} = Inventory.create_storage_location(%{ - name: "Drawer 2", - description: "Semiconductors and ICs", - parent_id: shelf_a.id -}) +{:ok, drawer_a2} = + Inventory.create_storage_location(%{ + name: "Drawer 2", + description: "Semiconductors and ICs", + parent_id: shelf_a.id + }) # Create boxes in Drawer A1 -{:ok, box_a1_1} = Inventory.create_storage_location(%{ - name: "Box 1", - description: "Through-hole resistors", - parent_id: drawer_a1.id -}) +{:ok, box_a1_1} = + Inventory.create_storage_location(%{ + name: "Box 1", + description: "Through-hole resistors", + parent_id: drawer_a1.id + }) -{:ok, _box_a1_2} = Inventory.create_storage_location(%{ - name: "Box 2", - description: "SMD resistors", - parent_id: drawer_a1.id -}) +{:ok, _box_a1_2} = + Inventory.create_storage_location(%{ + name: "Box 2", + description: "SMD resistors", + parent_id: drawer_a1.id + }) -{:ok, box_a1_3} = Inventory.create_storage_location(%{ - name: "Box 3", - description: "Ceramic capacitors", - parent_id: drawer_a1.id -}) +{:ok, box_a1_3} = + Inventory.create_storage_location(%{ + name: "Box 3", + description: "Ceramic capacitors", + parent_id: drawer_a1.id + }) # Create boxes in Drawer A2 -{:ok, box_a2_1} = Inventory.create_storage_location(%{ - name: "Box 1", - description: "Microcontrollers", - parent_id: drawer_a2.id -}) +{:ok, box_a2_1} = + Inventory.create_storage_location(%{ + name: "Box 1", + description: "Microcontrollers", + parent_id: drawer_a2.id + }) -{:ok, _box_a2_2} = Inventory.create_storage_location(%{ - name: "Box 2", - description: "Transistors and diodes", - parent_id: drawer_a2.id -}) +{:ok, _box_a2_2} = + Inventory.create_storage_location(%{ + name: "Box 2", + description: "Transistors and diodes", + parent_id: drawer_a2.id + }) # Create a DEEP storage location hierarchy to test fallback path (7+ levels) -{:ok, deep_loc_1} = Inventory.create_storage_location(%{name: "Deep Level 1", description: "Deep hierarchy test", parent_id: box_a1_3.id}) -{:ok, deep_loc_2} = Inventory.create_storage_location(%{name: "Deep Level 2", description: "Deep hierarchy test", parent_id: deep_loc_1.id}) -{:ok, deep_loc_3} = Inventory.create_storage_location(%{name: "Deep Level 3", description: "Deep hierarchy test", parent_id: deep_loc_2.id}) -{:ok, deep_loc_4} = Inventory.create_storage_location(%{name: "Deep Level 4", description: "Deep hierarchy test", parent_id: deep_loc_3.id}) -{:ok, deep_loc_5} = Inventory.create_storage_location(%{name: "Deep Level 5", description: "Deep hierarchy test", parent_id: deep_loc_4.id}) -{:ok, deep_loc_6} = Inventory.create_storage_location(%{name: "Deep Level 6", description: "Deep hierarchy test", parent_id: deep_loc_5.id}) -{:ok, deep_loc_7} = Inventory.create_storage_location(%{name: "Deep Level 7", description: "Deep hierarchy test - triggers fallback", parent_id: deep_loc_6.id}) +{:ok, deep_loc_1} = + Inventory.create_storage_location(%{ + name: "Deep Level 1", + description: "Deep hierarchy test", + parent_id: box_a1_3.id + }) + +{:ok, deep_loc_2} = + Inventory.create_storage_location(%{ + name: "Deep Level 2", + description: "Deep hierarchy test", + parent_id: deep_loc_1.id + }) + +{:ok, deep_loc_3} = + Inventory.create_storage_location(%{ + name: "Deep Level 3", + description: "Deep hierarchy test", + parent_id: deep_loc_2.id + }) + +{:ok, deep_loc_4} = + Inventory.create_storage_location(%{ + name: "Deep Level 4", + description: "Deep hierarchy test", + parent_id: deep_loc_3.id + }) + +{:ok, deep_loc_5} = + Inventory.create_storage_location(%{ + name: "Deep Level 5", + description: "Deep hierarchy test", + parent_id: deep_loc_4.id + }) + +{:ok, deep_loc_6} = + Inventory.create_storage_location(%{ + name: "Deep Level 6", + description: "Deep hierarchy test", + parent_id: deep_loc_5.id + }) + +{:ok, deep_loc_7} = + Inventory.create_storage_location(%{ + name: "Deep Level 7", + description: "Deep hierarchy test - triggers fallback", + parent_id: deep_loc_6.id + }) # Create sample components sample_components = [ @@ -162,7 +268,8 @@ sample_components = [ keywords: "microcontroller avr atmega328 arduino", storage_location_id: box_a2_1.id, count: 10, - datasheet_url: "https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf", + datasheet_url: + "https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf", category_id: semiconductors.id }, %{ @@ -264,7 +371,11 @@ IO.puts("") IO.puts("🎉 Database seeded successfully!") IO.puts("📊 Summary:") IO.puts(" Categories: #{length(Inventory.list_categories())}") -IO.puts(" Storage Locations: #{length(Inventory.list_storage_locations())} (with auto-assigned AprilTags)") + +IO.puts( + " Storage Locations: #{length(Inventory.list_storage_locations())} (with auto-assigned AprilTags)" +) + IO.puts(" Components: #{length(Inventory.list_components())}") IO.puts("") IO.puts("🏷️ AprilTag System:") diff --git a/test/components_elixir_web/controllers/error_html_test.exs b/test/components_elixir_web/controllers/error_html_test.exs index f98a9d0..44a8c86 100644 --- a/test/components_elixir_web/controllers/error_html_test.exs +++ b/test/components_elixir_web/controllers/error_html_test.exs @@ -9,6 +9,7 @@ defmodule ComponentsElixirWeb.ErrorHTMLTest do end test "renders 500.html" do - assert render_to_string(ComponentsElixirWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" + assert render_to_string(ComponentsElixirWeb.ErrorHTML, "500", "html", []) == + "Internal Server Error" end end diff --git a/test/components_elixir_web/controllers/error_json_test.exs b/test/components_elixir_web/controllers/error_json_test.exs index ca3abdb..deb11b5 100644 --- a/test/components_elixir_web/controllers/error_json_test.exs +++ b/test/components_elixir_web/controllers/error_json_test.exs @@ -2,7 +2,9 @@ defmodule ComponentsElixirWeb.ErrorJSONTest do use ComponentsElixirWeb.ConnCase, async: true test "renders 404" do - assert ComponentsElixirWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} + assert ComponentsElixirWeb.ErrorJSON.render("404.json", %{}) == %{ + errors: %{detail: "Not Found"} + } end test "renders 500" do