diff --git a/lib/scopes/core.ex b/lib/scopes/core.ex
new file mode 100644
index 0000000..b1dbe54
--- /dev/null
+++ b/lib/scopes/core.ex
@@ -0,0 +1,104 @@
+defmodule Scopes.Core do
+ @moduledoc """
+ The Core context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Scopes.Repo
+
+ alias Scopes.Core.Message
+
+ @doc """
+ Returns the list of messages.
+
+ ## Examples
+
+ iex> list_messages()
+ [%Message{}, ...]
+
+ """
+ def list_messages do
+ Repo.all(Message)
+ end
+
+ @doc """
+ Gets a single message.
+
+ Raises `Ecto.NoResultsError` if the Message does not exist.
+
+ ## Examples
+
+ iex> get_message!(123)
+ %Message{}
+
+ iex> get_message!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_message!(id), do: Repo.get!(Message, id)
+
+ @doc """
+ Creates a message.
+
+ ## Examples
+
+ iex> create_message(%{field: value})
+ {:ok, %Message{}}
+
+ iex> create_message(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_message(attrs \\ %{}) do
+ %Message{}
+ |> Message.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a message.
+
+ ## Examples
+
+ iex> update_message(message, %{field: new_value})
+ {:ok, %Message{}}
+
+ iex> update_message(message, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_message(%Message{} = message, attrs) do
+ message
+ |> Message.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a message.
+
+ ## Examples
+
+ iex> delete_message(message)
+ {:ok, %Message{}}
+
+ iex> delete_message(message)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_message(%Message{} = message) do
+ Repo.delete(message)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking message changes.
+
+ ## Examples
+
+ iex> change_message(message)
+ %Ecto.Changeset{data: %Message{}}
+
+ """
+ def change_message(%Message{} = message, attrs \\ %{}) do
+ Message.changeset(message, attrs)
+ end
+end
diff --git a/lib/scopes/core/message.ex b/lib/scopes/core/message.ex
new file mode 100644
index 0000000..baa4926
--- /dev/null
+++ b/lib/scopes/core/message.ex
@@ -0,0 +1,21 @@
+defmodule Scopes.Core.Message do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "messages" do
+ field :data, :map
+ field :domain, :string
+ field :item, :string
+ field :action, :string
+ field :class, :string
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(message, attrs) do
+ message
+ |> cast(attrs, [:domain, :action, :class, :item, :data])
+ |> validate_required([:domain, :action, :class, :item])
+ end
+end
diff --git a/lib/scopes_web/live/message_live/form_component.ex b/lib/scopes_web/live/message_live/form_component.ex
new file mode 100644
index 0000000..f215cfb
--- /dev/null
+++ b/lib/scopes_web/live/message_live/form_component.ex
@@ -0,0 +1,85 @@
+defmodule ScopesWeb.MessageLive.FormComponent do
+ use ScopesWeb, :live_component
+
+ alias Scopes.Core
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ {@title}
+ <:subtitle>Use this form to manage message records in your database.
+
+
+ <.simple_form
+ for={@form}
+ id="message-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <.input field={@form[:domain]} type="text" label="Domain" />
+ <.input field={@form[:action]} type="text" label="Action" />
+ <.input field={@form[:class]} type="text" label="Class" />
+ <.input field={@form[:item]} type="text" label="Item" />
+ <:actions>
+ <.button phx-disable-with="Saving...">Save Message
+
+
+
+ """
+ end
+
+ @impl true
+ def update(%{message: message} = assigns, socket) do
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign_new(:form, fn ->
+ to_form(Core.change_message(message))
+ end)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"message" => message_params}, socket) do
+ changeset = Core.change_message(socket.assigns.message, message_params)
+ {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
+ end
+
+ def handle_event("save", %{"message" => message_params}, socket) do
+ save_message(socket, socket.assigns.action, message_params)
+ end
+
+ defp save_message(socket, :edit, message_params) do
+ case Core.update_message(socket.assigns.message, message_params) do
+ {:ok, message} ->
+ notify_parent({:saved, message})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Message updated successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp save_message(socket, :new, message_params) do
+ case Core.create_message(message_params) do
+ {:ok, message} ->
+ notify_parent({:saved, message})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Message created successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
+end
diff --git a/lib/scopes_web/live/message_live/index.ex b/lib/scopes_web/live/message_live/index.ex
new file mode 100644
index 0000000..ede0569
--- /dev/null
+++ b/lib/scopes_web/live/message_live/index.ex
@@ -0,0 +1,47 @@
+defmodule ScopesWeb.MessageLive.Index do
+ use ScopesWeb, :live_view
+
+ alias Scopes.Core
+ alias Scopes.Core.Message
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, stream(socket, :messages, Core.list_messages())}
+ end
+
+ @impl true
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit Message")
+ |> assign(:message, Core.get_message!(id))
+ end
+
+ defp apply_action(socket, :new, _params) do
+ socket
+ |> assign(:page_title, "New Message")
+ |> assign(:message, %Message{})
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket
+ |> assign(:page_title, "Listing Messages")
+ |> assign(:message, nil)
+ end
+
+ @impl true
+ def handle_info({ScopesWeb.MessageLive.FormComponent, {:saved, message}}, socket) do
+ {:noreply, stream_insert(socket, :messages, message)}
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => id}, socket) do
+ message = Core.get_message!(id)
+ {:ok, _} = Core.delete_message(message)
+
+ {:noreply, stream_delete(socket, :messages, message)}
+ end
+end
diff --git a/lib/scopes_web/live/message_live/index.html.heex b/lib/scopes_web/live/message_live/index.html.heex
new file mode 100644
index 0000000..52ca8c4
--- /dev/null
+++ b/lib/scopes_web/live/message_live/index.html.heex
@@ -0,0 +1,45 @@
+<.header>
+ Listing Messages
+ <:actions>
+ <.link patch={~p"/messages/new"}>
+ <.button>New Message
+
+
+
+
+<.table
+ id="messages"
+ rows={@streams.messages}
+ row_click={fn {_id, message} -> JS.navigate(~p"/messages/#{message}") end}
+>
+ <:col :let={{_id, message}} label="Domain">{message.domain}
+ <:col :let={{_id, message}} label="Action">{message.action}
+ <:col :let={{_id, message}} label="Class">{message.class}
+ <:col :let={{_id, message}} label="Item">{message.item}
+ <:col :let={{_id, message}} label="Data">{message.data}
+ <:action :let={{_id, message}}>
+
+ <.link navigate={~p"/messages/#{message}"}>Show
+
+ <.link patch={~p"/messages/#{message}/edit"}>Edit
+
+ <:action :let={{id, message}}>
+ <.link
+ phx-click={JS.push("delete", value: %{id: message.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ Delete
+
+
+
+
+<.modal :if={@live_action in [:new, :edit]} id="message-modal" show on_cancel={JS.patch(~p"/messages")}>
+ <.live_component
+ module={ScopesWeb.MessageLive.FormComponent}
+ id={@message.id || :new}
+ title={@page_title}
+ action={@live_action}
+ message={@message}
+ patch={~p"/messages"}
+ />
+
diff --git a/lib/scopes_web/live/message_live/show.ex b/lib/scopes_web/live/message_live/show.ex
new file mode 100644
index 0000000..e119917
--- /dev/null
+++ b/lib/scopes_web/live/message_live/show.ex
@@ -0,0 +1,21 @@
+defmodule ScopesWeb.MessageLive.Show do
+ use ScopesWeb, :live_view
+
+ alias Scopes.Core
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, socket}
+ end
+
+ @impl true
+ def handle_params(%{"id" => id}, _, socket) do
+ {:noreply,
+ socket
+ |> assign(:page_title, page_title(socket.assigns.live_action))
+ |> assign(:message, Core.get_message!(id))}
+ end
+
+ defp page_title(:show), do: "Show Message"
+ defp page_title(:edit), do: "Edit Message"
+end
diff --git a/lib/scopes_web/live/message_live/show.html.heex b/lib/scopes_web/live/message_live/show.html.heex
new file mode 100644
index 0000000..6c27dd3
--- /dev/null
+++ b/lib/scopes_web/live/message_live/show.html.heex
@@ -0,0 +1,30 @@
+<.header>
+ Message {@message.id}
+ <:subtitle>This is a message record from your database.
+ <:actions>
+ <.link patch={~p"/messages/#{@message}/show/edit"} phx-click={JS.push_focus()}>
+ <.button>Edit message
+
+
+
+
+<.list>
+ <:item title="Domain">{@message.domain}
+ <:item title="Action">{@message.action}
+ <:item title="Class">{@message.class}
+ <:item title="Item">{@message.item}
+ <:item title="Data">{@message.data}
+
+
+<.back navigate={~p"/messages"}>Back to messages
+
+<.modal :if={@live_action == :edit} id="message-modal" show on_cancel={JS.patch(~p"/messages/#{@message}")}>
+ <.live_component
+ module={ScopesWeb.MessageLive.FormComponent}
+ id={@message.id}
+ title={@page_title}
+ action={@live_action}
+ message={@message}
+ patch={~p"/messages/#{@message}"}
+ />
+
diff --git a/lib/scopes_web/router.ex b/lib/scopes_web/router.ex
index 14d2e02..31d4e8f 100644
--- a/lib/scopes_web/router.ex
+++ b/lib/scopes_web/router.ex
@@ -72,6 +72,13 @@ defmodule ScopesWeb.Router do
live "/users/settings", UserSettingsLive, :edit
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
live "/bingo", Bingo
+
+ live "/messages", MessageLive.Index, :index
+ live "/messages/new", MessageLive.Index, :new
+ live "/messages/:id/edit", MessageLive.Index, :edit
+ live "/messages/:id", MessageLive.Show, :show
+ live "/messages/:id/show/edit", MessageLive.Show, :edit
+
end
end
diff --git a/priv/repo/migrations/20250614165203_create_messages.exs b/priv/repo/migrations/20250614165203_create_messages.exs
new file mode 100644
index 0000000..663d012
--- /dev/null
+++ b/priv/repo/migrations/20250614165203_create_messages.exs
@@ -0,0 +1,15 @@
+defmodule Scopes.Repo.Migrations.CreateMessages do
+ use Ecto.Migration
+
+ def change do
+ create table(:messages) do
+ add :domain, :string
+ add :action, :string
+ add :class, :string
+ add :item, :string
+ add :data, :map
+
+ timestamps(type: :utc_datetime)
+ end
+ end
+end
diff --git a/test/scopes/core_test.exs b/test/scopes/core_test.exs
new file mode 100644
index 0000000..3b20f07
--- /dev/null
+++ b/test/scopes/core_test.exs
@@ -0,0 +1,67 @@
+defmodule Scopes.CoreTest do
+ use Scopes.DataCase
+
+ alias Scopes.Core
+
+ describe "messages" do
+ alias Scopes.Core.Message
+
+ import Scopes.CoreFixtures
+
+ @invalid_attrs %{data: nil, domain: nil, item: nil, action: nil, class: nil}
+
+ test "list_messages/0 returns all messages" do
+ message = message_fixture()
+ assert Core.list_messages() == [message]
+ end
+
+ test "get_message!/1 returns the message with given id" do
+ message = message_fixture()
+ assert Core.get_message!(message.id) == message
+ end
+
+ test "create_message/1 with valid data creates a message" do
+ valid_attrs = %{data: %{}, domain: "some domain", item: "some item", action: "some action", class: "some class"}
+
+ assert {:ok, %Message{} = message} = Core.create_message(valid_attrs)
+ assert message.data == %{}
+ assert message.domain == "some domain"
+ assert message.item == "some item"
+ assert message.action == "some action"
+ assert message.class == "some class"
+ end
+
+ test "create_message/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Core.create_message(@invalid_attrs)
+ end
+
+ test "update_message/2 with valid data updates the message" do
+ message = message_fixture()
+ update_attrs = %{data: %{}, domain: "some updated domain", item: "some updated item", action: "some updated action", class: "some updated class"}
+
+ assert {:ok, %Message{} = message} = Core.update_message(message, update_attrs)
+ assert message.data == %{}
+ assert message.domain == "some updated domain"
+ assert message.item == "some updated item"
+ assert message.action == "some updated action"
+ assert message.class == "some updated class"
+ end
+
+ test "update_message/2 with invalid data returns error changeset" do
+ message = message_fixture()
+ assert {:error, %Ecto.Changeset{}} = Core.update_message(message, @invalid_attrs)
+ assert message == Core.get_message!(message.id)
+ end
+
+ test "delete_message/1 deletes the message" do
+ message = message_fixture()
+ assert {:ok, %Message{}} = Core.delete_message(message)
+ assert_raise Ecto.NoResultsError, fn -> Core.get_message!(message.id) end
+ end
+
+ test "change_message/1 returns a message changeset" do
+ message = message_fixture()
+ assert %Ecto.Changeset{} = Core.change_message(message)
+ end
+ end
+end
diff --git a/test/scopes_web/live/message_live_test.exs b/test/scopes_web/live/message_live_test.exs
new file mode 100644
index 0000000..7bf58fc
--- /dev/null
+++ b/test/scopes_web/live/message_live_test.exs
@@ -0,0 +1,113 @@
+defmodule ScopesWeb.MessageLiveTest do
+ use ScopesWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import Scopes.CoreFixtures
+
+ @create_attrs %{data: %{}, domain: "some domain", item: "some item", action: "some action", class: "some class"}
+ @update_attrs %{data: %{}, domain: "some updated domain", item: "some updated item", action: "some updated action", class: "some updated class"}
+ @invalid_attrs %{data: nil, domain: nil, item: nil, action: nil, class: nil}
+
+ defp create_message(_) do
+ message = message_fixture()
+ %{message: message}
+ end
+
+ describe "Index" do
+ setup [:create_message]
+
+ test "lists all messages", %{conn: conn, message: message} do
+ {:ok, _index_live, html} = live(conn, ~p"/messages")
+
+ assert html =~ "Listing Messages"
+ assert html =~ message.domain
+ end
+
+ test "saves new message", %{conn: conn} do
+ {:ok, index_live, _html} = live(conn, ~p"/messages")
+
+ assert index_live |> element("a", "New Message") |> render_click() =~
+ "New Message"
+
+ assert_patch(index_live, ~p"/messages/new")
+
+ assert index_live
+ |> form("#message-form", message: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert index_live
+ |> form("#message-form", message: @create_attrs)
+ |> render_submit()
+
+ assert_patch(index_live, ~p"/messages")
+
+ html = render(index_live)
+ assert html =~ "Message created successfully"
+ assert html =~ "some domain"
+ end
+
+ test "updates message in listing", %{conn: conn, message: message} do
+ {:ok, index_live, _html} = live(conn, ~p"/messages")
+
+ assert index_live |> element("#messages-#{message.id} a", "Edit") |> render_click() =~
+ "Edit Message"
+
+ assert_patch(index_live, ~p"/messages/#{message}/edit")
+
+ assert index_live
+ |> form("#message-form", message: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert index_live
+ |> form("#message-form", message: @update_attrs)
+ |> render_submit()
+
+ assert_patch(index_live, ~p"/messages")
+
+ html = render(index_live)
+ assert html =~ "Message updated successfully"
+ assert html =~ "some updated domain"
+ end
+
+ test "deletes message in listing", %{conn: conn, message: message} do
+ {:ok, index_live, _html} = live(conn, ~p"/messages")
+
+ assert index_live |> element("#messages-#{message.id} a", "Delete") |> render_click()
+ refute has_element?(index_live, "#messages-#{message.id}")
+ end
+ end
+
+ describe "Show" do
+ setup [:create_message]
+
+ test "displays message", %{conn: conn, message: message} do
+ {:ok, _show_live, html} = live(conn, ~p"/messages/#{message}")
+
+ assert html =~ "Show Message"
+ assert html =~ message.domain
+ end
+
+ test "updates message within modal", %{conn: conn, message: message} do
+ {:ok, show_live, _html} = live(conn, ~p"/messages/#{message}")
+
+ assert show_live |> element("a", "Edit") |> render_click() =~
+ "Edit Message"
+
+ assert_patch(show_live, ~p"/messages/#{message}/show/edit")
+
+ assert show_live
+ |> form("#message-form", message: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert show_live
+ |> form("#message-form", message: @update_attrs)
+ |> render_submit()
+
+ assert_patch(show_live, ~p"/messages/#{message}")
+
+ html = render(show_live)
+ assert html =~ "Message updated successfully"
+ assert html =~ "some updated domain"
+ end
+ end
+end
diff --git a/test/support/fixtures/core_fixtures.ex b/test/support/fixtures/core_fixtures.ex
new file mode 100644
index 0000000..dffc5b8
--- /dev/null
+++ b/test/support/fixtures/core_fixtures.ex
@@ -0,0 +1,24 @@
+defmodule Scopes.CoreFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `Scopes.Core` context.
+ """
+
+ @doc """
+ Generate a message.
+ """
+ def message_fixture(attrs \\ %{}) do
+ {:ok, message} =
+ attrs
+ |> Enum.into(%{
+ action: "some action",
+ class: "some class",
+ data: %{},
+ domain: "some domain",
+ item: "some item"
+ })
+ |> Scopes.Core.create_message()
+
+ message
+ end
+end