diff --git a/config/config.exs b/config/config.exs
index c88afd1..81bd5e8 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -84,8 +84,6 @@ config :goatmire, Goatmire.Repo,
migration_foreign_Key: [column: :id, type: :binary_id],
migration_timestamps: [type: :utc_datetime]
-config :rustler_precompiled, :force_build, typst: true
-
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
diff --git a/config/dev.exs b/config/dev.exs
index c65cf05..4fdd3b6 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -14,9 +14,7 @@ config :goatmire, Goatmire.Repo,
# watchers to your application. For example, we can use it
# to bundle .js and .css sources.
config :goatmire, GoatmireWeb.Endpoint,
- # Binding to loopback ipv4 address prevents access from other machines.
- # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
- http: [ip: {127, 0, 0, 1}, port: String.to_integer(System.get_env("PORT") || "4000")],
+ http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],
check_origin: false,
code_reloader: true,
debug_errors: true,
@@ -83,3 +81,19 @@ config :phoenix_live_view,
# Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false
+
+# MinIO (local S3) config for development
+# Uses mDNS hostname - works for both local (via hosts) and network devices
+minio_host = System.get_env("MINIO_HOST") || "pepe-laptop.local"
+
+config :ex_aws,
+ json_codec: Jason,
+ access_key_id: "minioadmin",
+ secret_access_key: "minioadmin"
+
+config :ex_aws, :s3,
+ scheme: "http://",
+ host: minio_host,
+ port: 9000,
+ region: "us-east-1",
+ bucket: "goatmire"
diff --git a/config/runtime.exs b/config/runtime.exs
index 42795ac..c9ae114 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -20,26 +20,26 @@ if System.get_env("PHX_SERVER") do
config :goatmire, GoatmireWeb.Endpoint, server: true
end
-# Tigris config
-config :ex_aws,
- debug_requests: true,
- json_codec: Jason,
- access_key_id:
- System.get_env("AWS_ACCESS_KEY_ID") ||
- raise("Missing env variable: AWS_ACCESS_KEY_ID"),
- secret_access_key:
- System.get_env("AWS_SECRET_ACCESS_KEY") ||
- raise("Missing env variable: AWS_SECRET_ACCESS_KEY")
+if config_env() == :prod do
+ # Tigris config
+ config :ex_aws,
+ debug_requests: true,
+ json_codec: Jason,
+ access_key_id:
+ System.get_env("AWS_ACCESS_KEY_ID") ||
+ raise("Missing env variable: AWS_ACCESS_KEY_ID"),
+ secret_access_key:
+ System.get_env("AWS_SECRET_ACCESS_KEY") ||
+ raise("Missing env variable: AWS_SECRET_ACCESS_KEY")
-config :ex_aws, :s3,
- scheme: "https://",
- host: "fly.storage.tigris.dev",
- region: "auto",
- bucket:
- System.get_env("BUCKET_NAME") ||
- raise("Missing env variable: BUCKET_NAME")
+ config :ex_aws, :s3,
+ scheme: "https://",
+ host: "fly.storage.tigris.dev",
+ region: "auto",
+ bucket:
+ System.get_env("BUCKET_NAME") ||
+ raise("Missing env variable: BUCKET_NAME")
-if config_env() == :prod do
database_path =
System.get_env("DATABASE_PATH") ||
raise """
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 0000000..f85078b
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,35 @@
+services:
+ minio:
+ image: minio/minio:RELEASE.2024-11-07T00-52-20Z
+ container_name: goatmire-minio
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ environment:
+ MINIO_ROOT_USER: minioadmin
+ MINIO_ROOT_PASSWORD: minioadmin
+ command: server /data --console-address ":9001"
+ volumes:
+ - minio-data:/data
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+
+ create-bucket:
+ image: minio/mc:RELEASE.2025-08-13T08-35-41Z
+ container_name: goatmire-create-bucket
+ depends_on:
+ minio:
+ condition: service_healthy
+ entrypoint: >
+ /bin/sh -c "
+ mc alias set local http://minio:9000 minioadmin minioadmin;
+ mc mb local/goatmire --ignore-existing;
+ mc anonymous set public local/goatmire;
+ exit 0;
+ "
+
+volumes:
+ minio-data:
\ No newline at end of file
diff --git a/lib/goatmire/accounts/user.ex b/lib/goatmire/accounts/user.ex
index 9e58b5d..939f3ec 100644
--- a/lib/goatmire/accounts/user.ex
+++ b/lib/goatmire/accounts/user.ex
@@ -2,6 +2,9 @@ defmodule Goatmire.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+
schema "users" do
field :email, :string
field :password, :string, virtual: true, redact: true
diff --git a/lib/goatmire/accounts/user_token.ex b/lib/goatmire/accounts/user_token.ex
index c17d860..ef3b013 100644
--- a/lib/goatmire/accounts/user_token.ex
+++ b/lib/goatmire/accounts/user_token.ex
@@ -3,6 +3,9 @@ defmodule Goatmire.Accounts.UserToken do
import Ecto.Query
alias Goatmire.Accounts.UserToken
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+
@hash_algorithm :sha256
@rand_size 32
diff --git a/lib/goatmire/gallery_slideshow.ex b/lib/goatmire/gallery_slideshow.ex
index 2edd6ca..cfe9d58 100644
--- a/lib/goatmire/gallery_slideshow.ex
+++ b/lib/goatmire/gallery_slideshow.ex
@@ -1,6 +1,8 @@
defmodule Goatmire.GallerySlideshow do
use GenServer
+ require Logger
+
alias Goatmire.Media
def start_link(opts \\ []) do
@@ -16,13 +18,22 @@ defmodule Goatmire.GallerySlideshow do
end
@impl true
- def handle_info(:update, state) do
- [image | rest] =
- case state.photos do
- [] -> Media.list_approved_images() |> Enum.shuffle()
- photos -> photos
- end
+ def handle_info(:update, %{photos: []} = state) do
+ case Media.list_approved_images() |> Enum.shuffle() do
+ [] ->
+ Logger.warning("No approved images available for slideshow")
+ {:noreply, state}
+
+ [image | rest] ->
+ broadcast_and_update(image, rest, state)
+ end
+ end
+
+ def handle_info(:update, %{photos: [image | rest]} = state) do
+ broadcast_and_update(image, rest, state)
+ end
+ defp broadcast_and_update(image, rest, state) do
GoatmireWeb.Endpoint.broadcast("device_gallery", "image", %{
url: Goatmire.Utils.presigned_url(image.dithered_key)
})
diff --git a/lib/goatmire/media.ex b/lib/goatmire/media.ex
index 497629f..caf061f 100644
--- a/lib/goatmire/media.ex
+++ b/lib/goatmire/media.ex
@@ -53,6 +53,55 @@ defmodule Goatmire.Media do
iex> create_image(%{field: bad_value})
{:error, %Ecto.Changeset{}}
+ """
+ def create_image(attrs) do
+ %Image{}
+ |> Image.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a image.
+
+ ## Examples
+
+ iex> update_image(image, %{field: new_value})
+ {:ok, %Image{}}
+
+ iex> update_image(image, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_image(%Image{} = image, attrs) do
+ image
+ |> Image.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Returns all images.
+
+ ## Examples
+
+ iex> list_images()
+ [%Image{}, ...]
+
+ """
+ def list_images do
+ Repo.all(Image)
+ end
+
+ @doc """
+ Creates a image and processes it.
+
+ ## Examples
+
+ iex> create_and_process_image(%{field: value})
+ {:ok, %Image{}}
+
+ iex> create_and_process_image(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
"""
def create_and_process_image(attrs) do
%Image{}
@@ -72,16 +121,13 @@ defmodule Goatmire.Media do
end
@doc """
- Updates a image.
+ Updates image processing keys.
## Examples
- iex> update_image(image, %{field: new_value})
+ iex> update_image_keys(image, %{dithered_key: "..."})
{:ok, %Image{}}
- iex> update_image(image, %{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
"""
def update_image_keys(%Image{} = image, attrs) do
image
diff --git a/lib/goatmire_web/live/device_config_live.ex b/lib/goatmire_web/live/device_config_live.ex
index 4bd520e..e440475 100644
--- a/lib/goatmire_web/live/device_config_live.ex
+++ b/lib/goatmire_web/live/device_config_live.ex
@@ -93,6 +93,20 @@ defmodule GoatmireWeb.DeviceConfigLive do
+
+