From 570ec811be61734009ded8239030713d00cb1580 Mon Sep 17 00:00:00 2001 From: Adam Wight Date: Tue, 7 Oct 2025 08:07:17 +0200 Subject: [PATCH 1/2] Use upstream exqlite The changes have been merged into upstream: https://github.com/elixir-sqlite/exqlite/compare/main...elixir-desktop:exqlite:main#diff-76ed074a9305c04054cdebb9e9aad2d818052b07091de1f20cad0bbac34ffb52R62 Merged in elixir-sqlite/exqlite#214 and 898bd41101b738097bf --- mix.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/mix.exs b/mix.exs index 27d78bf..1f26613 100644 --- a/mix.exs +++ b/mix.exs @@ -76,7 +76,6 @@ defmodule Todo.MixProject do defp deps do deps_list = [ {:ecto_sqlite3, "~> 0.12"}, - {:exqlite, github: "elixir-desktop/exqlite", override: true}, # {:desktop, path: "../desktop"}, # {:desktop, "~> 1.5"}, {:desktop, github: "elixir-desktop/desktop"}, From f123a7745edd73b210538c7b5b8cf1b24d2d8d18 Mon Sep 17 00:00:00 2001 From: Adam Wight Date: Tue, 7 Oct 2025 18:23:15 +0200 Subject: [PATCH 2/2] Heavy updates to liveview 1.x Copy over most of a fresh phoenix 1.8 generated project skeleton. --- config/config.exs | 18 +- lib/todo_app.ex | 16 +- lib/todo_app/menu_bar.ex | 10 +- lib/todo_web.ex | 89 ++-- lib/todo_web/components/core_components.ex | 472 ++++++++++++++++++ lib/todo_web/components/layouts.ex | 67 +++ .../components/layouts/root.html.heex | 20 + lib/todo_web/controllers/error.ex | 11 - lib/todo_web/controllers/error_html.ex | 24 + lib/todo_web/controllers/error_json.ex | 21 + lib/todo_web/controllers/page_controller.ex | 7 + lib/todo_web/controllers/page_html.ex | 10 + lib/todo_web/endpoint.ex | 10 +- lib/todo_web/live/todo_live.ex | 23 + lib/todo_web/live/todo_live.html.leex | 16 - lib/todo_web/router.ex | 25 +- lib/todo_web/sup.ex | 21 - lib/todo_web/telemetry.ex | 93 ++++ lib/todo_web/templates/layout/app.html.eex | 5 - lib/todo_web/templates/layout/live.html.leex | 11 - lib/todo_web/templates/layout/root.html.heex | 18 - lib/todo_web/views/error_helpers.ex | 51 -- lib/todo_web/views/error_view.ex | 20 - lib/todo_web/views/layout_view.ex | 3 - mix.exs | 36 +- mix.lock | 35 +- 26 files changed, 893 insertions(+), 239 deletions(-) create mode 100644 lib/todo_web/components/core_components.ex create mode 100644 lib/todo_web/components/layouts.ex create mode 100644 lib/todo_web/components/layouts/root.html.heex delete mode 100644 lib/todo_web/controllers/error.ex create mode 100644 lib/todo_web/controllers/error_html.ex create mode 100644 lib/todo_web/controllers/error_json.ex create mode 100644 lib/todo_web/controllers/page_controller.ex create mode 100644 lib/todo_web/controllers/page_html.ex delete mode 100644 lib/todo_web/live/todo_live.html.leex delete mode 100644 lib/todo_web/sup.ex create mode 100644 lib/todo_web/telemetry.ex delete mode 100644 lib/todo_web/templates/layout/app.html.eex delete mode 100644 lib/todo_web/templates/layout/live.html.leex delete mode 100644 lib/todo_web/templates/layout/root.html.heex delete mode 100644 lib/todo_web/views/error_helpers.ex delete mode 100644 lib/todo_web/views/error_view.ex delete mode 100644 lib/todo_web/views/layout_view.ex diff --git a/config/config.exs b/config/config.exs index 27c9905..4365804 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,17 +1,18 @@ import Config config :esbuild, - version: "0.12.18", + version: "0.25.4", default: [ - args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets), + args: + ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.), cd: Path.expand("../assets", __DIR__), - env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} + env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]} ] config :dart_sass, version: "1.61.0", default: [ - args: ~w(css/app.scss ../priv/static/assets/app.css), + args: ~w(css/app.scss ../priv/static/assets/css/app.css), cd: Path.expand("../assets", __DIR__) ] @@ -27,12 +28,17 @@ config :logger, :console, # Configures the endpoint config :todo_app, TodoWeb.Endpoint, + # url: [host: "localhost", port: 10_000 + :rand.uniform(45_000)], # because of the iOS rebind - this is now a fixed port, but randomly selected http: [ip: {127, 0, 0, 1}, port: 10_000 + :rand.uniform(45_000)], - render_errors: [view: TodoWeb.ErrorView, accepts: ~w(html json), layout: false], + adapter: Bandit.PhoenixAdapter, + render_errors: [ + formats: [html: TodoWeb.ErrorHTML, json: TodoWeb.ErrorJSON], + layout: false + ], pubsub_server: TodoApp.PubSub, live_view: [signing_salt: "sWpG9ljX"], - secret_key_base: :crypto.strong_rand_bytes(32), + secret_key_base: :crypto.strong_rand_bytes(64), server: true config :phoenix, :json_library, Jason diff --git a/lib/todo_app.ex b/lib/todo_app.ex index b649489..4834f44 100644 --- a/lib/todo_app.ex +++ b/lib/todo_app.ex @@ -23,14 +23,24 @@ defmodule TodoApp do Desktop.identify_default_locale(TodoWeb.Gettext) File.mkdir_p!(config_dir()) + # TODO: move to runtime.exs Application.put_env(:todo_app, TodoApp.Repo, database: Path.join(config_dir(), "/database.sq3") ) - {:ok, sup} = Supervisor.start_link([TodoApp.Repo], name: __MODULE__, strategy: :one_for_one) - TodoApp.Repo.initialize() + :ets.new(:session, [:named_table, :public, read_concurrency: true]) - {:ok, _} = Supervisor.start_child(sup, TodoWeb.Sup) + {:ok, sup} = Supervisor.start_link([ + TodoWeb.Telemetry, + TodoApp.Repo, + {DNSCluster, query: Application.get_env(:todo, :dns_cluster_query) || :ignore}, + {Phoenix.PubSub, name: TodoApp.PubSub}, + # Start a worker by calling: Todo.Worker.start_link(arg) + # {Todo.Worker, arg}, + # Start to serve requests, typically the last entry + TodoWeb.Endpoint + ], name: __MODULE__, strategy: :one_for_one) + TodoApp.Repo.initialize() {:ok, _} = Supervisor.start_child(sup, { diff --git a/lib/todo_app/menu_bar.ex b/lib/todo_app/menu_bar.ex index 9756667..a3be970 100644 --- a/lib/todo_app/menu_bar.ex +++ b/lib/todo_app/menu_bar.ex @@ -16,15 +16,15 @@ defmodule TodoApp.MenuBar do <%= item.text %> + >{item.text} <% end %>
- <%= gettext "Quit" %> + {gettext "Quit"} - <%= gettext "Show Notification" %> - <%= gettext "Show Observer" %> - <%= gettext "Open Browser" %> + {gettext "Show Notification"} + {gettext "Show Observer"} + {gettext "Open Browser"} """ diff --git a/lib/todo_web.ex b/lib/todo_web.ex index c833b99..b3cf2a5 100644 --- a/lib/todo_web.ex +++ b/lib/todo_web.ex @@ -17,41 +17,42 @@ defmodule TodoWeb do and import those modules here. """ - def controller do + def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) + + def router do quote do - use Phoenix.Controller, namespace: TodoWeb + use Phoenix.Router, helpers: false + # Import common connection and controller functions to use in pipelines import Plug.Conn - use Gettext, backend: TodoWeb.Gettext - alias TodoWeb.Router.Helpers, as: Routes + import Phoenix.Controller + import Phoenix.LiveView.Router end end - def view do + def channel do quote do - use Phoenix.View, - root: "lib/todo_web/templates", - namespace: TodoWeb + use Phoenix.Channel + end + end - # Import convenience functions from controllers - import Phoenix.Controller, - only: [view_module: 1, view_template: 1] + def controller do + quote do + use Phoenix.Controller, formats: [:html, :json] + + use Gettext, backend: TodoWeb.Gettext - alias Phoenix.Flash + import Plug.Conn - # Include shared imports and aliases for views - unquote(view_helpers()) - alias TodoWeb.Router.Helpers, as: Routes + unquote(verified_routes()) end end def live_view do quote do - use Phoenix.LiveView, - layout: {TodoWeb.LayoutView, :live} + use Phoenix.LiveView - unquote(view_helpers()) - alias TodoWeb.Router.Helpers, as: Routes + unquote(html_helpers()) end end @@ -59,42 +60,48 @@ defmodule TodoWeb do quote do use Phoenix.LiveComponent - unquote(view_helpers()) - alias TodoWeb.Router.Helpers, as: Routes + unquote(html_helpers()) end end - def router do + def html do quote do - use Phoenix.Router + use Phoenix.Component - import Plug.Conn - import Phoenix.Controller - import Phoenix.LiveView.Router + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_csrf_token: 0, view_module: 1, view_template: 1] + + # Include general helpers for rendering HTML + unquote(html_helpers()) end end - def channel do + defp html_helpers do quote do - use Phoenix.Channel + # Translation use Gettext, backend: TodoWeb.Gettext - end - end - defp view_helpers do - quote do - # Use all HTML functionality (forms, tags, etc) - use Phoenix.HTML + # HTML escaping functionality + import Phoenix.HTML + # Core UI components + import TodoWeb.CoreComponents - # Import LiveView helpers (live_render, live_component, live_patch, etc) - import Phoenix.LiveView.Helpers - import Phoenix.Component + # Common modules used in templates + alias Phoenix.LiveView.JS + alias TodoWeb.Layouts - # Import basic rendering functionality (render, render_layout, etc) - import Phoenix.View + # Routes generation with the ~p sigil + unquote(verified_routes()) + end + end - import TodoWeb.ErrorHelpers - use Gettext, backend: TodoWeb.Gettext + def verified_routes do + quote do + use Phoenix.VerifiedRoutes, + endpoint: TodoWeb.Endpoint, + router: TodoWeb.Router, + statics: TodoWeb.static_paths() end end diff --git a/lib/todo_web/components/core_components.ex b/lib/todo_web/components/core_components.ex new file mode 100644 index 0000000..69852d8 --- /dev/null +++ b/lib/todo_web/components/core_components.ex @@ -0,0 +1,472 @@ +defmodule TodoWeb.CoreComponents do + @moduledoc """ + Provides core UI components. + + At first glance, this module may seem daunting, but its goal is to provide + core building blocks for your application, such as tables, forms, and + inputs. The components consist mostly of markup and are well-documented + with doc strings and declarative assigns. You may customize and style + them in any way you want, based on your application growth and needs. + + The foundation for styling is Tailwind CSS, a utility-first CSS framework, + augmented with daisyUI, a Tailwind CSS plugin that provides UI components + and themes. Here are useful references: + + * [daisyUI](https://daisyui.com/docs/intro/) - a good place to get + started and see the available components. + + * [Tailwind CSS](https://tailwindcss.com) - the foundational framework + we build on. You will use it for layout, sizing, flexbox, grid, and + spacing. + + * [Heroicons](https://heroicons.com) - see `icon/1` for usage. + + * [Phoenix.Component](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) - + the component system used by Phoenix. Some components, such as `<.link>` + and `<.form>`, are defined there. + + """ + use Phoenix.Component + use Gettext, backend: TodoWeb.Gettext + + alias Phoenix.LiveView.JS + + @doc """ + Renders flash notices. + + ## Examples + + <.flash kind={:info} flash={@flash} /> + <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back! + """ + attr :id, :string, doc: "the optional id of flash container" + attr :flash, :map, default: %{}, doc: "the map of flash messages to display" + attr :title, :string, default: nil + attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup" + attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container" + + slot :inner_block, doc: "the optional inner block that renders the flash message" + + def flash(assigns) do + assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end) + + ~H""" +
hide("##{@id}")} + role="alert" + class="toast toast-top toast-end z-50" + {@rest} + > +
+ <.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" /> + <.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" /> +
+

{@title}

+

{msg}

+
+
+ +
+
+ """ + end + + @doc """ + Renders a button with navigation support. + + ## Examples + + <.button>Send! + <.button phx-click="go" variant="primary">Send! + <.button navigate={~p"/"}>Home + """ + attr :rest, :global, include: ~w(href navigate patch method download name value disabled) + attr :class, :string + attr :variant, :string, values: ~w(primary) + slot :inner_block, required: true + + def button(%{rest: rest} = assigns) do + variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"} + + assigns = + assign_new(assigns, :class, fn -> + ["btn", Map.fetch!(variants, assigns[:variant])] + end) + + if rest[:href] || rest[:navigate] || rest[:patch] do + ~H""" + <.link class={@class} {@rest}> + {render_slot(@inner_block)} + + """ + else + ~H""" + + """ + end + end + + @doc """ + Renders an input with label and error messages. + + A `Phoenix.HTML.FormField` may be passed as argument, + which is used to retrieve the input name, id, and values. + Otherwise all attributes may be passed explicitly. + + ## Types + + This function accepts all HTML input types, considering that: + + * You may also set `type="select"` to render a ` + + {@label} + + + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + def input(%{type: "select"} = assigns) do + ~H""" +
+ + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + def input(%{type: "textarea"} = assigns) do + ~H""" +
+ + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + # All other inputs text, datetime-local, url, password, etc. are handled here... + def input(assigns) do + ~H""" +
+ + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + # Helper used by inputs to generate form errors + defp error(assigns) do + ~H""" +

+ <.icon name="hero-exclamation-circle" class="size-5" /> + {render_slot(@inner_block)} +

+ """ + end + + @doc """ + Renders a header with title. + """ + slot :inner_block, required: true + slot :subtitle + slot :actions + + def header(assigns) do + ~H""" +
+
+

+ {render_slot(@inner_block)} +

+

+ {render_slot(@subtitle)} +

+
+
{render_slot(@actions)}
+
+ """ + end + + @doc """ + Renders a table with generic styling. + + ## Examples + + <.table id="users" rows={@users}> + <:col :let={user} label="id">{user.id} + <:col :let={user} label="username">{user.username} + + """ + attr :id, :string, required: true + attr :rows, :list, required: true + attr :row_id, :any, default: nil, doc: "the function for generating the row id" + attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" + + attr :row_item, :any, + default: &Function.identity/1, + doc: "the function for mapping each row before calling the :col and :action slots" + + slot :col, required: true do + attr :label, :string + end + + slot :action, doc: "the slot for showing user actions in the last table column" + + def table(assigns) do + assigns = + with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do + assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) + end + + ~H""" + + + + + + + + + + + + + +
{col[:label]} + {gettext("Actions")} +
+ {render_slot(col, @row_item.(row))} + +
+ <%= for action <- @action do %> + {render_slot(action, @row_item.(row))} + <% end %> +
+
+ """ + end + + @doc """ + Renders a data list. + + ## Examples + + <.list> + <:item title="Title">{@post.title} + <:item title="Views">{@post.views} + + """ + slot :item, required: true do + attr :title, :string, required: true + end + + def list(assigns) do + ~H""" + + """ + end + + @doc """ + Renders a [Heroicon](https://heroicons.com). + + Heroicons come in three styles – outline, solid, and mini. + By default, the outline style is used, but solid and mini may + be applied by using the `-solid` and `-mini` suffix. + + You can customize the size and colors of the icons by setting + width, height, and background color classes. + + Icons are extracted from the `deps/heroicons` directory and bundled within + your compiled app.css by the plugin in `assets/vendor/heroicons.js`. + + ## Examples + + <.icon name="hero-x-mark" /> + <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> + """ + attr :name, :string, required: true + attr :class, :string, default: "size-4" + + def icon(%{name: "hero-" <> _} = assigns) do + ~H""" + + """ + end + + ## JS Commands + + def show(js \\ %JS{}, selector) do + JS.show(js, + to: selector, + time: 300, + transition: + {"transition-all ease-out duration-300", + "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", + "opacity-100 translate-y-0 sm:scale-100"} + ) + end + + def hide(js \\ %JS{}, selector) do + JS.hide(js, + to: selector, + time: 200, + transition: + {"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100", + "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} + ) + end + + @doc """ + Translates an error message using gettext. + """ + def translate_error({msg, opts}) do + # When using gettext, we typically pass the strings we want + # to translate as a static argument: + # + # # Translate the number of files with plural rules + # dngettext("errors", "1 file", "%{count} files", count) + # + # However the error messages in our forms and APIs are generated + # dynamically, so we need to translate them by calling Gettext + # with our gettext backend as first argument. Translations are + # available in the errors.po file (as we use the "errors" domain). + if count = opts[:count] do + Gettext.dngettext(TodoWeb.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(TodoWeb.Gettext, "errors", msg, opts) + end + end + + @doc """ + Translates the errors for a field from a keyword list of errors. + """ + def translate_errors(errors, field) when is_list(errors) do + for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) + end +end diff --git a/lib/todo_web/components/layouts.ex b/lib/todo_web/components/layouts.ex new file mode 100644 index 0000000..88bb626 --- /dev/null +++ b/lib/todo_web/components/layouts.ex @@ -0,0 +1,67 @@ +defmodule TodoWeb.Layouts do + @moduledoc """ + This module holds layouts and related functionality + used by your application. + """ + use TodoWeb, :html + + # Embed all files in layouts/* within this module. + # The default root.html.heex file contains the HTML + # skeleton of your application, namely HTML headers + # and other static content. + embed_templates "layouts/*" + + def app(assigns) do + ~H""" +
+ <%= @inner_content %> + {render_slot(@inner_block)} +
+ + <.flash_group flash={@flash} /> + """ + end + + @doc """ + Shows the flash group with standard titles and content. + + ## Examples + + <.flash_group flash={@flash} /> + """ + attr :flash, :map, required: true, doc: "the map of flash messages" + attr :id, :string, default: "flash-group", doc: "the optional id of flash container" + + def flash_group(assigns) do + ~H""" +
+ <.flash kind={:info} flash={@flash} /> + <.flash kind={:error} flash={@flash} /> + + <.flash + id="client-error" + kind={:error} + title={gettext("We can't find the internet")} + phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")} + phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})} + hidden + > + {gettext("Attempting to reconnect")} + <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> + + + <.flash + id="server-error" + kind={:error} + title={gettext("Something went wrong!")} + phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")} + phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})} + hidden + > + {gettext("Attempting to reconnect")} + <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> + +
+ """ + end +end diff --git a/lib/todo_web/components/layouts/root.html.heex b/lib/todo_web/components/layouts/root.html.heex new file mode 100644 index 0000000..d3c263d --- /dev/null +++ b/lib/todo_web/components/layouts/root.html.heex @@ -0,0 +1,20 @@ + + + + + + + + <.live_title suffix={gettext(" · Your Todos")} > + {assigns[:page_title] || "Todo"} + + + + + +
+ {@inner_content} +
+ + diff --git a/lib/todo_web/controllers/error.ex b/lib/todo_web/controllers/error.ex deleted file mode 100644 index ceea2f2..0000000 --- a/lib/todo_web/controllers/error.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule TodoWeb.Error do - use TodoWeb, :controller - - @moduledoc """ - Error message module - """ - - def index(conn, _opts) do - send_resp(conn, 401, "Unauthorized") - end -end diff --git a/lib/todo_web/controllers/error_html.ex b/lib/todo_web/controllers/error_html.ex new file mode 100644 index 0000000..de4bd18 --- /dev/null +++ b/lib/todo_web/controllers/error_html.ex @@ -0,0 +1,24 @@ +defmodule TodoWeb.ErrorHTML do + @moduledoc """ + This module is invoked by your endpoint in case of errors on HTML requests. + + See config/config.exs. + """ + use TodoWeb, :html + + # If you want to customize your error pages, + # uncomment the embed_templates/1 call below + # and add pages to the error directory: + # + # * lib/todo_web/controllers/error_html/404.html.heex + # * lib/todo_web/controllers/error_html/500.html.heex + # + # embed_templates "error_html/*" + + # The default is to render a plain text page based on + # the template name. For example, "404.html" becomes + # "Not Found". + def render(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end diff --git a/lib/todo_web/controllers/error_json.ex b/lib/todo_web/controllers/error_json.ex new file mode 100644 index 0000000..ef8ebaf --- /dev/null +++ b/lib/todo_web/controllers/error_json.ex @@ -0,0 +1,21 @@ +defmodule TodoWeb.ErrorJSON do + @moduledoc """ + This module is invoked by your endpoint in case of errors on JSON requests. + + See config/config.exs. + """ + + # If you want to customize a particular status code, + # you may add your own clauses, such as: + # + # def render("500.json", _assigns) do + # %{errors: %{detail: "Internal Server Error"}} + # end + + # By default, Phoenix returns the status message from + # the template name. For example, "404.json" becomes + # "Not Found". + def render(template, _assigns) do + %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} + end +end diff --git a/lib/todo_web/controllers/page_controller.ex b/lib/todo_web/controllers/page_controller.ex new file mode 100644 index 0000000..337c24b --- /dev/null +++ b/lib/todo_web/controllers/page_controller.ex @@ -0,0 +1,7 @@ +defmodule TodoWeb.PageController do + use TodoWeb, :controller + + def home(conn, _params) do + render(conn, :todo) + end +end diff --git a/lib/todo_web/controllers/page_html.ex b/lib/todo_web/controllers/page_html.ex new file mode 100644 index 0000000..df34803 --- /dev/null +++ b/lib/todo_web/controllers/page_html.ex @@ -0,0 +1,10 @@ +defmodule TodoWeb.PageHTML do + @moduledoc """ + This module contains pages rendered by PageController. + + See the `page_html` directory for all templates available. + """ + use TodoWeb, :html + + embed_templates "page_html/*" +end diff --git a/lib/todo_web/endpoint.ex b/lib/todo_web/endpoint.ex index 1a22604..af997ba 100644 --- a/lib/todo_web/endpoint.ex +++ b/lib/todo_web/endpoint.ex @@ -19,8 +19,8 @@ defmodule TodoWeb.Endpoint do plug Plug.Static, at: "/", from: :todo_app, - gzip: false, - only: ~w(assets fonts images favicon.ico robots.txt) + gzip: not code_reloading?, + only: TodoWeb.static_paths() # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. @@ -28,9 +28,15 @@ defmodule TodoWeb.Endpoint do socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket plug Phoenix.LiveReloader plug Phoenix.CodeReloader + plug Phoenix.Ecto.CheckRepoStatus, otp_app: :todo end + plug Phoenix.LiveDashboard.RequestLogger, + param_key: "request_logger", + cookie_key: "request_logger" + plug Plug.RequestId + plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], diff --git a/lib/todo_web/live/todo_live.ex b/lib/todo_web/live/todo_live.ex index 408f53d..fa5231e 100644 --- a/lib/todo_web/live/todo_live.ex +++ b/lib/todo_web/live/todo_live.ex @@ -7,6 +7,29 @@ defmodule TodoWeb.TodoLive do @impl true + def render(assigns) do + ~H""" +
+

{gettext "My Todo List"}

+
+ + +
+
+ +
    + <%= for item <- @todos do %> +
  • {item.text} + × +
  • + <% end %> +
+ """ + end + + @impl true + def mount(_args, _session, socket) do todos = TodoApp.Todo.all_todos() TodoApp.Todo.subscribe() diff --git a/lib/todo_web/live/todo_live.html.leex b/lib/todo_web/live/todo_live.html.leex deleted file mode 100644 index c87dd4d..0000000 --- a/lib/todo_web/live/todo_live.html.leex +++ /dev/null @@ -1,16 +0,0 @@ -
-

<%= gettext "My Todo List" %>

-
- "> - -
-
- -
    - <%= for item <- @todos do %> -
  • <%= item.text %> - × -
  • - <% end %> -
diff --git a/lib/todo_web/router.ex b/lib/todo_web/router.ex index 26a1c94..22ffdbf 100644 --- a/lib/todo_web/router.ex +++ b/lib/todo_web/router.ex @@ -5,7 +5,7 @@ defmodule TodoWeb.Router do plug :accepts, ["html"] plug :fetch_session plug :fetch_live_flash - plug :put_root_layout, {TodoWeb.LayoutView, :root} + plug :put_root_layout, html: {TodoWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers end @@ -16,6 +16,29 @@ defmodule TodoWeb.Router do scope "/", TodoWeb do pipe_through :browser + live "/", TodoLive end + + # Other scopes may use custom stacks. + # scope "/api", TodoWeb do + # pipe_through :api + # end + + # Enable LiveDashboard and Swoosh mailbox preview in development + if Application.compile_env(:todo, :dev_routes) do + # If you want to use the LiveDashboard in production, you should put + # it behind authentication and allow only admins to access it. + # If your application does not have an admins-only section yet, + # you can use Plug.BasicAuth to set up some basic authentication + # as long as you are also using SSL (which you should anyway). + import Phoenix.LiveDashboard.Router + + scope "/dev" do + pipe_through :browser + + live_dashboard "/dashboard", metrics: TodoWeb.Telemetry + forward "/mailbox", Plug.Swoosh.MailboxPreview + end + end end diff --git a/lib/todo_web/sup.ex b/lib/todo_web/sup.ex deleted file mode 100644 index 08104fa..0000000 --- a/lib/todo_web/sup.ex +++ /dev/null @@ -1,21 +0,0 @@ -defmodule TodoWeb.Sup do - use Supervisor - - @moduledoc """ - Supervisor for the WebApp - """ - - def start_link([]) do - Supervisor.start_link(__MODULE__, [], name: __MODULE__) - end - - def init([]) do - children = [ - {Phoenix.PubSub, name: TodoApp.PubSub}, - TodoWeb.Endpoint - ] - - :session = :ets.new(:session, [:named_table, :public, read_concurrency: true]) - Supervisor.init(children, strategy: :one_for_one, max_restarts: 1000) - end -end diff --git a/lib/todo_web/telemetry.ex b/lib/todo_web/telemetry.ex new file mode 100644 index 0000000..9ac7aa5 --- /dev/null +++ b/lib/todo_web/telemetry.ex @@ -0,0 +1,93 @@ +defmodule TodoWeb.Telemetry do + use Supervisor + import Telemetry.Metrics + + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end + + @impl true + def init(_arg) do + children = [ + # Telemetry poller will execute the given period measurements + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} + # Add reporters as children of your supervision tree. + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + def metrics do + [ + # Phoenix Metrics + summary("phoenix.endpoint.start.system_time", + unit: {:native, :millisecond} + ), + summary("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.start.system_time", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.exception.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.stop.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.socket_connected.duration", + unit: {:native, :millisecond} + ), + sum("phoenix.socket_drain.count"), + summary("phoenix.channel_joined.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.channel_handled_in.duration", + tags: [:event], + unit: {:native, :millisecond} + ), + + # Database Metrics + summary("todo.repo.query.total_time", + unit: {:native, :millisecond}, + description: "The sum of the other measurements" + ), + summary("todo.repo.query.decode_time", + unit: {:native, :millisecond}, + description: "The time spent decoding the data received from the database" + ), + summary("todo.repo.query.query_time", + unit: {:native, :millisecond}, + description: "The time spent executing the query" + ), + summary("todo.repo.query.queue_time", + unit: {:native, :millisecond}, + description: "The time spent waiting for a database connection" + ), + summary("todo.repo.query.idle_time", + unit: {:native, :millisecond}, + description: + "The time the connection spent waiting before being checked out for the query" + ), + + # VM Metrics + summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.total_run_queue_lengths.total"), + summary("vm.total_run_queue_lengths.cpu"), + summary("vm.total_run_queue_lengths.io") + ] + end + + defp periodic_measurements do + [ + # A module, function and arguments to be invoked periodically. + # This function must call :telemetry.execute/3 and a metric must be added above. + # {TodoWeb, :count_users, []} + ] + end +end diff --git a/lib/todo_web/templates/layout/app.html.eex b/lib/todo_web/templates/layout/app.html.eex deleted file mode 100644 index f45e43f..0000000 --- a/lib/todo_web/templates/layout/app.html.eex +++ /dev/null @@ -1,5 +0,0 @@ -
- - - <%= @inner_content %> -
diff --git a/lib/todo_web/templates/layout/live.html.leex b/lib/todo_web/templates/layout/live.html.leex deleted file mode 100644 index 2838827..0000000 --- a/lib/todo_web/templates/layout/live.html.leex +++ /dev/null @@ -1,11 +0,0 @@ -
- - - - - <%= @inner_content %> -
diff --git a/lib/todo_web/templates/layout/root.html.heex b/lib/todo_web/templates/layout/root.html.heex deleted file mode 100644 index 84ffdb9..0000000 --- a/lib/todo_web/templates/layout/root.html.heex +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - <%= csrf_meta_tag() %> - <.live_title suffix={gettext(" · Your Todos")} ><%= assigns[:page_title] || "Todo" %> - - - - - -
- <%= @inner_content %> -
- - diff --git a/lib/todo_web/views/error_helpers.ex b/lib/todo_web/views/error_helpers.ex deleted file mode 100644 index 9d6abc8..0000000 --- a/lib/todo_web/views/error_helpers.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule TodoWeb.ErrorHelpers do - @moduledoc """ - Conveniences for translating and building error messages. - """ - - use Phoenix.HTML - - @doc """ - Generates tag for inlined form input errors. - """ - def error_tag(form, field) do - Enum.map(Keyword.get_values(form.errors, field), fn error -> - content_tag(:span, translate_error(error), - class: "invalid-feedback", - phx_feedback_for: input_id(form, field) - ) - end) - end - - @doc """ - Translates an error message using gettext. - """ - def translate_error(msg) when is_binary(msg) do - translate_error({msg, []}) - end - - def translate_error({msg, opts}) do - # When using gettext, we typically pass the strings we want - # to translate as a static argument: - # - # # Translate "is invalid" in the "errors" domain - # dgettext("errors", "is invalid") - # - # # Translate the number of files with plural rules - # dngettext("errors", "1 file", "%{count} files", count) - # - # Because the error messages we show in our forms and APIs - # are defined inside Ecto, we need to translate them dynamically. - # This requires us to call the Gettext module passing our gettext - # backend as first argument. - # - # Note we use the "errors" domain, which means translations - # should be written to the errors.po file. The :count option is - # set by Ecto and indicates we should also apply plural rules. - if count = opts[:count] do - Gettext.dngettext(TodoWeb.Gettext, "errors", msg, msg, count, opts) - else - Gettext.dgettext(TodoWeb.Gettext, "errors", msg, opts) - end - end -end diff --git a/lib/todo_web/views/error_view.ex b/lib/todo_web/views/error_view.ex deleted file mode 100644 index ce12428..0000000 --- a/lib/todo_web/views/error_view.ex +++ /dev/null @@ -1,20 +0,0 @@ -defmodule TodoWeb.ErrorView do - use TodoWeb, :view - - # If you want to customize a particular status code - # for a certain format, you may uncomment below. - # def render("500.html", _assigns) do - # "Internal Server Error" - # end - - def render("404.html", _assigns) do - "Not Found" - end - - # By default, Phoenix returns the status message from - # the template name. For example, "404.html" becomes - # "Not Found". - def template_not_found(template, _assigns) do - Phoenix.Controller.status_message_from_template(template) - end -end diff --git a/lib/todo_web/views/layout_view.ex b/lib/todo_web/views/layout_view.ex deleted file mode 100644 index ce43104..0000000 --- a/lib/todo_web/views/layout_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule TodoWeb.LayoutView do - use TodoWeb, :view -end diff --git a/mix.exs b/mix.exs index 1f26613..c8ebb29 100644 --- a/mix.exs +++ b/mix.exs @@ -1,14 +1,15 @@ defmodule Todo.MixProject do use Mix.Project - @version "1.2.0" + @version "1.3.0" def project do [ app: :todo_app, version: @version, - elixir: "~> 1.14", + elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), - compilers: Mix.compilers(), + compilers: [:phoenix_live_view] ++ Mix.compilers(), + listeners: [Phoenix.CodeReloader], start_permanent: Mix.env() == :prod, deps: deps(), aliases: aliases(), @@ -25,6 +26,12 @@ defmodule Todo.MixProject do ] end + def cli do + [ + preferred_envs: [precommit: :test] + ] + end + # Specifies which paths to compile per environment. defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] @@ -35,6 +42,7 @@ defmodule Todo.MixProject do mod: {TodoApp, []}, extra_applications: [ :logger, + :runtime_tools, :ssl, :crypto, :sasl, @@ -75,7 +83,6 @@ defmodule Todo.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do deps_list = [ - {:ecto_sqlite3, "~> 0.12"}, # {:desktop, path: "../desktop"}, # {:desktop, "~> 1.5"}, {:desktop, github: "elixir-desktop/desktop"}, @@ -83,17 +90,22 @@ defmodule Todo.MixProject do # {:desktop_deployment, path: "../deployment", runtime: false}, # Phoenix - {:phoenix, "~> 1.7"}, - {:phoenix_live_view, "~> 0.20"}, - {:phoenix_html, "~> 3.3"}, - {:phoenix_view, "~> 2.0"}, - {:phoenix_live_reload, "~> 1.5", only: [:dev]}, - {:gettext, "~> 0.23"}, - {:plug_cowboy, "~> 2.6"}, + {:phoenix, "~> 1.8"}, + {:phoenix_ecto, "~> 4.5"}, + {:ecto_sqlite3, "~> 0.22"}, + {:phoenix_html, "~> 4.1"}, + {:phoenix_live_dashboard, "~> 0.8.3"}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:phoenix_live_view, "~> 1.1"}, + {:telemetry_metrics, "~> 1.0"}, + {:telemetry_poller, "~> 1.0"}, + {:gettext, "~> 0.26"}, {:jason, "~> 1.4"}, + {:dns_cluster, "~> 0.2.0"}, + {:bandit, "~> 1.5"}, # Assets - {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, + {:esbuild, "~> 0.10", runtime: Mix.env() == :dev}, {:dart_sass, "~> 0.7", runtime: Mix.env() == :dev}, # Credo diff --git a/mix.lock b/mix.lock index de57354..14348a5 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,8 @@ %{ + "bandit": {:hex, :bandit, "1.8.0", "c2e93d7e3c5c794272fa4623124f827c6f24b643acc822be64c826f9447d92fb", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8458ff4eed20ff2a2ea69d4854883a077c33ea42b51f6811b044ceee0fa15422"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, + "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, @@ -8,48 +10,55 @@ "cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"}, "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, "dart_sass": {:hex, :dart_sass, "0.7.0", "7979e056cb74fd6843e1c72db763cffc7726a9192a657735b7d24c0d9c26a1ce", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4a8e70bca41aa00846398abdf5ad8a64d7907a0f7bf40145cd2e40d5971629f2"}, - "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, + "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, "dbus": {:hex, :dbus, "0.8.0", "7c800681f35d909c199265e55a8ee4aea9ebe4acccce77a0740f89f29cc57648", [:make], [], "hexpm", "a9784f2d9717ffa1f74169144a226c39633ac0d9c7fe8cb3594aeb89c827cca5"}, "debouncer": {:hex, :debouncer, "0.1.8", "ab7f6eef7c9f029929bb2b737de6dc4bf12ac4ddedb49194ba00f4240b226b5a", [:mix], [], "hexpm", "77c4be1d10f984aec0dfbc50d43c16d7512605cabe6d79cadc0c8f4ba95d4906"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "desktop": {:git, "https://github.com/elixir-desktop/desktop.git", "4d5768dad0380ae0f3f4a80b0e2628c9872e1e25", []}, "desktop_deployment": {:git, "https://github.com/elixir-desktop/deployment.git", "29aa9b2de6465242b50a7b37d91b2141c505b347", []}, - "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, - "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, - "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.18.1", "63c4b01cbb67f5e7eecde6819bc050fbb835141b534b779f80f40d479cdce669", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "ce223f0eda408437e7dc2b5f2a9dab306c7a709dc865f96172ba5eadfea3ce48"}, + "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, + "ecto": {:hex, :ecto, "3.13.3", "6a983f0917f8bdc7a89e96f2bf013f220503a0da5d8623224ba987515b3f0d80", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1927db768f53a88843ff25b6ba7946599a8ca8a055f69ad8058a1432a399af94"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.22.0", "edab2d0f701b7dd05dcf7e2d97769c106aff62b5cfddc000d1dd6f46b9cbd8c3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5af9e031bffcc5da0b7bca90c271a7b1e7c04a93fecf7f6cd35bc1b1921a64bd"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, - "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"}, + "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "ex_dbus": {:hex, :ex_dbus, "0.1.4", "053df83d45b27ba0b9b6ef55a47253922069a3ace12a2a7dd30d3aff58301e17", [:mix], [{:dbus, "~> 0.8.0", [hex: :dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "d8baeaf465eab57b70a47b70e29fdfef6eb09ba110fc37176eebe6ac7874d6d5"}, "ex_sni": {:hex, :ex_sni, "0.2.9", "81f9421035dd3edb6d69f1a4dd5f53c7071b41628130d32ba5ab7bb4bfdc2da0", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_dbus, "~> 0.1", [hex: :ex_dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "921d67d913765ed20ea8354fd1798dabc957bf66990a6842d6aaa7cd5ee5bc06"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, - "exqlite": {:git, "https://github.com/elixir-desktop/exqlite.git", "1caf1f42395fff8a68ac5a509f7294090a6f6f0d", []}, + "exqlite": {:hex, :exqlite, "0.33.1", "0465fdb997be174edeba6a27496fa27dfe8bc79ef1324a723daa8f0e8579da24", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "b3db0c9ae6e5ee7cf84dd0a1b6dc7566b80912eb7746d45370f5666ed66700f9"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "httpoison": {:hex, :httpoison, "2.2.2", "15420e9e5bbb505b931b2f589dc8be0c3b21e2a91a2c6ba882d99bf8f3ad499d", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "de7ac49fe2ffd89219972fdf39b268582f6f7f68d8cd29b4482dacca1ce82324"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "libpe": {:hex, :libpe, "1.2.3", "b0669eabc59470a63c5d2d83fe405d43e0f0de4068c5e21511e16e6948d818ee", [:mix], [], "hexpm", "494dc8082917ea645031b5ade1d8cc038548bf3d39066b2d87bebbe1bcde1c4a"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "oncrash": {:hex, :oncrash, "0.1.0", "9cf4ae8eba4ea250b579470172c5e9b8c75418b2264de7dbcf42e408d62e30fb", [:mix], [], "hexpm", "6968e775491cd857f9b6ff940bf2574fd1c2fab84fa7e14d5f56c39174c00018"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "phoenix": {:hex, :phoenix, "1.7.20", "6bababaf27d59f5628f9b608de902a021be2cecefb8231e1dbdc0a2e2e480e9b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "6be2ab98302e8784a31829e0d50d8bdfa81a23cd912c395bafd8b8bfb5a086c2"}, - "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, + "phoenix": {:hex, :phoenix, "1.8.1", "865473a60a979551a4879db79fbfb4503e41cd809e77c85af79716578b6a456d", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "84d77d2b2e77c3c7e7527099bd01ef5c8560cd149c036d6b3a40745f11cd2fb2"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.17", "f396bbdaf4ba227b82251eb75ac0afa6b3da5e509bc0d030206374237dfc9450", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61d741ffb78c85fdbca0de084da6a48f8ceb5261a79165b5a0b59e5f65ce98b"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.13", "11f48f8fbe5d7d0731d4e122a692e0f9ae8d5f98c54d573d29046833c34eada3", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9d06e93573b8419ef8ee832b4a9bfe0def10b15f7c129ea477dfa54f0136f7ec"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, - "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"}, - "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "saxy": {:hex, :saxy, "1.4.0", "c7203ad20001f72eaaad07d08f82be063fa94a40924e6bb39d93d55f979abcba", [:mix], [], "hexpm", "3fe790354d3f2234ad0b5be2d99822a23fa2d4e8ccd6657c672901dac172e9a9"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, + "thousand_island": {:hex, :thousand_island, "1.4.1", "8df065e627407e281f7935da5ad0f3842d10eb721afa92e760b720d71e2e37aa", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "204a8640e5d2818589b87286ae66160978628d7edf6095181cbe0440765fb6c1"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},