···11+# StatusphereElixir
22+33+To start your Phoenix server:
44+55+* Run `mix setup` to install and setup dependencies
66+* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
77+88+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
99+1010+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
1111+1212+## Learn more
1313+1414+* Official website: https://www.phoenixframework.org/
1515+* Guides: https://hexdocs.pm/phoenix/overview.html
1616+* Docs: https://hexdocs.pm/phoenix
1717+* Forum: https://elixirforum.com/c/phoenix-forum
1818+* Source: https://github.com/phoenixframework/phoenix
+105
assets/css/app.css
···11+/* See the Tailwind configuration guide for advanced usage
22+ https://tailwindcss.com/docs/configuration */
33+44+@import "tailwindcss" source(none);
55+@source "../css";
66+@source "../js";
77+@source "../../lib/statusphere_elixir_web";
88+99+/* A Tailwind plugin that makes "hero-#{ICON}" classes available.
1010+ The heroicons installation itself is managed by your mix.exs */
1111+@plugin "../vendor/heroicons";
1212+1313+/* daisyUI Tailwind Plugin. You can update this file by fetching the latest version with:
1414+ curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
1515+ Make sure to look at the daisyUI changelog: https://daisyui.com/docs/changelog/ */
1616+@plugin "../vendor/daisyui" {
1717+ themes: false;
1818+}
1919+2020+/* daisyUI theme plugin. You can update this file by fetching the latest version with:
2121+ curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.js
2222+ We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired
2323+ on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */
2424+@plugin "../vendor/daisyui-theme" {
2525+ name: "dark";
2626+ default: false;
2727+ prefersdark: true;
2828+ color-scheme: "dark";
2929+ --color-base-100: oklch(30.33% 0.016 252.42);
3030+ --color-base-200: oklch(25.26% 0.014 253.1);
3131+ --color-base-300: oklch(20.15% 0.012 254.09);
3232+ --color-base-content: oklch(97.807% 0.029 256.847);
3333+ --color-primary: oklch(58% 0.233 277.117);
3434+ --color-primary-content: oklch(96% 0.018 272.314);
3535+ --color-secondary: oklch(58% 0.233 277.117);
3636+ --color-secondary-content: oklch(96% 0.018 272.314);
3737+ --color-accent: oklch(60% 0.25 292.717);
3838+ --color-accent-content: oklch(96% 0.016 293.756);
3939+ --color-neutral: oklch(37% 0.044 257.287);
4040+ --color-neutral-content: oklch(98% 0.003 247.858);
4141+ --color-info: oklch(58% 0.158 241.966);
4242+ --color-info-content: oklch(97% 0.013 236.62);
4343+ --color-success: oklch(60% 0.118 184.704);
4444+ --color-success-content: oklch(98% 0.014 180.72);
4545+ --color-warning: oklch(66% 0.179 58.318);
4646+ --color-warning-content: oklch(98% 0.022 95.277);
4747+ --color-error: oklch(58% 0.253 17.585);
4848+ --color-error-content: oklch(96% 0.015 12.422);
4949+ --radius-selector: 0.25rem;
5050+ --radius-field: 0.25rem;
5151+ --radius-box: 0.5rem;
5252+ --size-selector: 0.21875rem;
5353+ --size-field: 0.21875rem;
5454+ --border: 1.5px;
5555+ --depth: 1;
5656+ --noise: 0;
5757+}
5858+5959+@plugin "../vendor/daisyui-theme" {
6060+ name: "light";
6161+ default: true;
6262+ prefersdark: false;
6363+ color-scheme: "light";
6464+ --color-base-100: oklch(98% 0 0);
6565+ --color-base-200: oklch(96% 0.001 286.375);
6666+ --color-base-300: oklch(92% 0.004 286.32);
6767+ --color-base-content: oklch(21% 0.006 285.885);
6868+ --color-primary: oklch(70% 0.213 47.604);
6969+ --color-primary-content: oklch(98% 0.016 73.684);
7070+ --color-secondary: oklch(55% 0.027 264.364);
7171+ --color-secondary-content: oklch(98% 0.002 247.839);
7272+ --color-accent: oklch(0% 0 0);
7373+ --color-accent-content: oklch(100% 0 0);
7474+ --color-neutral: oklch(44% 0.017 285.786);
7575+ --color-neutral-content: oklch(98% 0 0);
7676+ --color-info: oklch(62% 0.214 259.815);
7777+ --color-info-content: oklch(97% 0.014 254.604);
7878+ --color-success: oklch(70% 0.14 182.503);
7979+ --color-success-content: oklch(98% 0.014 180.72);
8080+ --color-warning: oklch(66% 0.179 58.318);
8181+ --color-warning-content: oklch(98% 0.022 95.277);
8282+ --color-error: oklch(58% 0.253 17.585);
8383+ --color-error-content: oklch(96% 0.015 12.422);
8484+ --radius-selector: 0.25rem;
8585+ --radius-field: 0.25rem;
8686+ --radius-box: 0.5rem;
8787+ --size-selector: 0.21875rem;
8888+ --size-field: 0.21875rem;
8989+ --border: 1.5px;
9090+ --depth: 1;
9191+ --noise: 0;
9292+}
9393+9494+/* Add variants based on LiveView classes */
9595+@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
9696+@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
9797+@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
9898+9999+/* Use the data attribute for dark mode */
100100+@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
101101+102102+/* Make LiveView wrapper divs transparent for layout */
103103+[data-phx-session], [data-phx-teleported-src] { display: contents }
104104+105105+/* This file is for your main application CSS */
+83
assets/js/app.js
···11+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
22+// to get started and then uncomment the line below.
33+// import "./user_socket.js"
44+55+// You can include dependencies in two ways.
66+//
77+// The simplest option is to put them in assets/vendor and
88+// import them using relative paths:
99+//
1010+// import "../vendor/some-package.js"
1111+//
1212+// Alternatively, you can `npm install some-package --prefix assets` and import
1313+// them using a path starting with the package name:
1414+//
1515+// import "some-package"
1616+//
1717+// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
1818+// To load it, simply add a second `<link>` to your `root.html.heex` file.
1919+2020+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
2121+import "phoenix_html"
2222+// Establish Phoenix Socket and LiveView configuration.
2323+import {Socket} from "phoenix"
2424+import {LiveSocket} from "phoenix_live_view"
2525+import {hooks as colocatedHooks} from "phoenix-colocated/statusphere_elixir"
2626+import topbar from "../vendor/topbar"
2727+2828+const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
2929+const liveSocket = new LiveSocket("/live", Socket, {
3030+ longPollFallbackMs: 2500,
3131+ params: {_csrf_token: csrfToken},
3232+ hooks: {...colocatedHooks},
3333+})
3434+3535+// Show progress bar on live navigation and form submits
3636+topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
3737+window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
3838+window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
3939+4040+// connect if there are any LiveViews on the page
4141+liveSocket.connect()
4242+4343+// expose liveSocket on window for web console debug logs and latency simulation:
4444+// >> liveSocket.enableDebug()
4545+// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
4646+// >> liveSocket.disableLatencySim()
4747+window.liveSocket = liveSocket
4848+4949+// The lines below enable quality of life phoenix_live_reload
5050+// development features:
5151+//
5252+// 1. stream server logs to the browser console
5353+// 2. click on elements to jump to their definitions in your code editor
5454+//
5555+if (process.env.NODE_ENV === "development") {
5656+ window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
5757+ // Enable server log streaming to client.
5858+ // Disable with reloader.disableServerLogs()
5959+ reloader.enableServerLogs()
6060+6161+ // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
6262+ //
6363+ // * click with "c" key pressed to open at caller location
6464+ // * click with "d" key pressed to open at function component definition location
6565+ let keyDown
6666+ window.addEventListener("keydown", e => keyDown = e.key)
6767+ window.addEventListener("keyup", _e => keyDown = null)
6868+ window.addEventListener("click", e => {
6969+ if(keyDown === "c"){
7070+ e.preventDefault()
7171+ e.stopImmediatePropagation()
7272+ reloader.openEditorAtCaller(e.target)
7373+ } else if(keyDown === "d"){
7474+ e.preventDefault()
7575+ e.stopImmediatePropagation()
7676+ reloader.openEditorAtDef(e.target)
7777+ }
7878+ }, true)
7979+8080+ window.liveReloader = reloader
8181+ })
8282+}
8383+
+32
assets/tsconfig.json
···11+// This file is needed on most editors to enable the intelligent autocompletion
22+// of LiveView's JavaScript API methods. You can safely delete it if you don't need it.
33+//
44+// Note: This file assumes a basic esbuild setup without node_modules.
55+// We include a generic paths alias to deps to mimic how esbuild resolves
66+// the Phoenix and LiveView JavaScript assets.
77+// If you have a package.json in your project, you should remove the
88+// paths configuration and instead add the phoenix dependencies to the
99+// dependencies section of your package.json:
1010+//
1111+// {
1212+// ...
1313+// "dependencies": {
1414+// ...,
1515+// "phoenix": "../deps/phoenix",
1616+// "phoenix_html": "../deps/phoenix_html",
1717+// "phoenix_live_view": "../deps/phoenix_live_view"
1818+// }
1919+// }
2020+//
2121+// Feel free to adjust this configuration however you need.
2222+{
2323+ "compilerOptions": {
2424+ "baseUrl": ".",
2525+ "paths": {
2626+ "*": ["../deps/*"]
2727+ },
2828+ "allowJs": true,
2929+ "noEmit": true
3030+ },
3131+ "include": ["js/**/*"]
3232+}
···11+# This file is responsible for configuring your application
22+# and its dependencies with the aid of the Config module.
33+#
44+# This configuration file is loaded before any dependency and
55+# is restricted to this project.
66+77+# General application configuration
88+import Config
99+1010+config :statusphere_elixir,
1111+ ecto_repos: [StatusphereElixir.Repo],
1212+ generators: [timestamp_type: :utc_datetime, binary_id: true]
1313+1414+# Configure the endpoint
1515+config :statusphere_elixir, StatusphereElixirWeb.Endpoint,
1616+ url: [host: "localhost"],
1717+ adapter: Bandit.PhoenixAdapter,
1818+ render_errors: [
1919+ formats: [html: StatusphereElixirWeb.ErrorHTML, json: StatusphereElixirWeb.ErrorJSON],
2020+ layout: false
2121+ ],
2222+ pubsub_server: StatusphereElixir.PubSub,
2323+ live_view: [signing_salt: "t+91lIqk"]
2424+2525+# Configure esbuild (the version is required)
2626+config :esbuild,
2727+ version: "0.25.4",
2828+ statusphere_elixir: [
2929+ args:
3030+ ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
3131+ cd: Path.expand("../assets", __DIR__),
3232+ env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
3333+ ]
3434+3535+# Configure tailwind (the version is required)
3636+config :tailwind,
3737+ version: "4.1.12",
3838+ statusphere_elixir: [
3939+ args: ~w(
4040+ --input=assets/css/app.css
4141+ --output=priv/static/assets/css/app.css
4242+ ),
4343+ cd: Path.expand("..", __DIR__)
4444+ ],
4545+ version_check: false,
4646+ path:
4747+ System.get_env(
4848+ "TAILWINDCSS_PATH",
4949+ Path.expand("../assets/node_modules/.bin/tailwindcss", __DIR__)
5050+ )
5151+5252+# Configure Elixir's Logger
5353+config :logger, :default_formatter,
5454+ format: "$time $metadata[$level] $message\n",
5555+ metadata: [:request_id]
5656+5757+# Use Jason for JSON parsing in Phoenix
5858+config :phoenix, :json_library, Jason
5959+6060+# Import environment specific config. This must remain at the bottom
6161+# of this file so it overrides the configuration defined above.
6262+import_config "#{config_env()}.exs"
+84
config/dev.exs
···11+import Config
22+33+# Configure your database
44+config :statusphere_elixir, StatusphereElixir.Repo,
55+ database: Path.expand("../statusphere_elixir_dev.db", __DIR__),
66+ pool_size: 5,
77+ stacktrace: true,
88+ show_sensitive_data_on_connection_error: true
99+1010+# For development, we disable any cache and enable
1111+# debugging and code reloading.
1212+#
1313+# The watchers configuration can be used to run external
1414+# watchers to your application. For example, we can use it
1515+# to bundle .js and .css sources.
1616+config :statusphere_elixir, StatusphereElixirWeb.Endpoint,
1717+ # Binding to loopback ipv4 address prevents access from other machines.
1818+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
1919+ http: [ip: {127, 0, 0, 1}],
2020+ check_origin: false,
2121+ code_reloader: true,
2222+ debug_errors: true,
2323+ secret_key_base: "YjHndGQyYIEkOsJtZzle8dUtbznbvM2CwGhF7NSoWxgSTIGpouNbVIND7cAmnWfB",
2424+ watchers: [
2525+ esbuild: {Esbuild, :install_and_run, [:statusphere_elixir, ~w(--sourcemap=inline --watch)]},
2626+ tailwind: {Tailwind, :run, [:statusphere_elixir, ~w(--watch)]}
2727+ ]
2828+2929+# ## SSL Support
3030+#
3131+# In order to use HTTPS in development, a self-signed
3232+# certificate can be generated by running the following
3333+# Mix task:
3434+#
3535+# mix phx.gen.cert
3636+#
3737+# Run `mix help phx.gen.cert` for more information.
3838+#
3939+# The `http:` config above can be replaced with:
4040+#
4141+# https: [
4242+# port: 4001,
4343+# cipher_suite: :strong,
4444+# keyfile: "priv/cert/selfsigned_key.pem",
4545+# certfile: "priv/cert/selfsigned.pem"
4646+# ],
4747+#
4848+# If desired, both `http:` and `https:` keys can be
4949+# configured to run both http and https servers on
5050+# different ports.
5151+5252+# Reload browser tabs when matching files change.
5353+config :statusphere_elixir, StatusphereElixirWeb.Endpoint,
5454+ live_reload: [
5555+ web_console_logger: true,
5656+ patterns: [
5757+ # Static assets, except user uploads
5858+ ~r"priv/static/(?!uploads/).*\.(js|css|png|jpeg|jpg|gif|svg)$",
5959+ # Router, Controllers, LiveViews and LiveComponents
6060+ ~r"lib/statusphere_elixir_web/router\.ex$",
6161+ ~r"lib/statusphere_elixir_web/(controllers|live|components)/.*\.(ex|heex)$"
6262+ ]
6363+ ]
6464+6565+# Enable dev routes for dashboard and mailbox
6666+config :statusphere_elixir, dev_routes: true
6767+6868+# Do not include metadata nor timestamps in development logs
6969+config :logger, :default_formatter, format: "[$level] $message\n"
7070+7171+# Set a higher stacktrace during development. Avoid configuring such
7272+# in production as building large stacktraces may be expensive.
7373+config :phoenix, :stacktrace_depth, 20
7474+7575+# Initialize plugs at runtime for faster development compilation
7676+config :phoenix, :plug_init_mode, :runtime
7777+7878+config :phoenix_live_view,
7979+ # Include debug annotations and locations in rendered markup.
8080+ # Changing this configuration will require mix clean and a full recompile.
8181+ debug_heex_annotations: true,
8282+ debug_attributes: true,
8383+ # Enable helpful, but potentially expensive runtime checks
8484+ enable_expensive_runtime_checks: true
+25
config/prod.exs
···11+import Config
22+33+# Note we also include the path to a cache manifest
44+# containing the digested version of static files. This
55+# manifest is generated by the `mix assets.deploy` task,
66+# which you should run after static files are built and
77+# before starting your production server.
88+config :statusphere_elixir, StatusphereElixirWeb.Endpoint,
99+ cache_static_manifest: "priv/static/cache_manifest.json"
1010+1111+# Force using SSL in production. This also sets the "strict-security-transport" header,
1212+# known as HSTS. If you have a health check endpoint, you may want to exclude it below.
1313+# Note `:force_ssl` is required to be set at compile-time.
1414+config :statusphere_elixir, StatusphereElixirWeb.Endpoint,
1515+ force_ssl: [rewrite_on: [:x_forwarded_proto]],
1616+ exclude: [
1717+ # paths: ["/health"],
1818+ hosts: ["localhost", "127.0.0.1"]
1919+ ]
2020+2121+# Do not print debug messages in production
2222+config :logger, level: :info
2323+2424+# Runtime production configuration, including reading
2525+# of environment variables, is done on config/runtime.exs.
+96
config/runtime.exs
···11+import Config
22+33+# config/runtime.exs is executed for all environments, including
44+# during releases. It is executed after compilation and before the
55+# system starts, so it is typically used to load production configuration
66+# and secrets from environment variables or elsewhere. Do not define
77+# any compile-time configuration in here, as it won't be applied.
88+# The block below contains prod specific runtime configuration.
99+1010+# ## Using releases
1111+#
1212+# If you use `mix release`, you need to explicitly enable the server
1313+# by passing the PHX_SERVER=true when you start it:
1414+#
1515+# PHX_SERVER=true bin/statusphere_elixir start
1616+#
1717+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
1818+# script that automatically sets the env var above.
1919+if System.get_env("PHX_SERVER") do
2020+ config :statusphere_elixir, StatusphereElixirWeb.Endpoint, server: true
2121+end
2222+2323+config :statusphere_elixir, StatusphereElixirWeb.Endpoint,
2424+ http: [port: String.to_integer(System.get_env("PORT", "4000"))]
2525+2626+if config_env() == :prod do
2727+ database_path =
2828+ System.get_env("DATABASE_PATH") ||
2929+ raise """
3030+ environment variable DATABASE_PATH is missing.
3131+ For example: /etc/statusphere_elixir/statusphere_elixir.db
3232+ """
3333+3434+ config :statusphere_elixir, StatusphereElixir.Repo,
3535+ database: database_path,
3636+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
3737+3838+ # The secret key base is used to sign/encrypt cookies and other secrets.
3939+ # A default value is used in config/dev.exs and config/test.exs but you
4040+ # want to use a different value for prod and you most likely don't want
4141+ # to check this value into version control, so we use an environment
4242+ # variable instead.
4343+ secret_key_base =
4444+ System.get_env("SECRET_KEY_BASE") ||
4545+ raise """
4646+ environment variable SECRET_KEY_BASE is missing.
4747+ You can generate one by calling: mix phx.gen.secret
4848+ """
4949+5050+ host = System.get_env("PHX_HOST") || "example.com"
5151+5252+ config :statusphere_elixir, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
5353+5454+ config :statusphere_elixir, StatusphereElixirWeb.Endpoint,
5555+ url: [host: host, port: 443, scheme: "https"],
5656+ http: [
5757+ # Enable IPv6 and bind on all interfaces.
5858+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
5959+ # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
6060+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
6161+ ip: {0, 0, 0, 0, 0, 0, 0, 0}
6262+ ],
6363+ secret_key_base: secret_key_base
6464+6565+ # ## SSL Support
6666+ #
6767+ # To get SSL working, you will need to add the `https` key
6868+ # to your endpoint configuration:
6969+ #
7070+ # config :statusphere_elixir, StatusphereElixirWeb.Endpoint,
7171+ # https: [
7272+ # ...,
7373+ # port: 443,
7474+ # cipher_suite: :strong,
7575+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
7676+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
7777+ # ]
7878+ #
7979+ # The `cipher_suite` is set to `:strong` to support only the
8080+ # latest and more secure SSL ciphers. This means old browsers
8181+ # and clients may not be supported. You can set it to
8282+ # `:compatible` for wider support.
8383+ #
8484+ # `:keyfile` and `:certfile` expect an absolute path to the key
8585+ # and cert in disk or a relative path inside priv, for example
8686+ # "priv/ssl/server.key". For all supported SSL configuration
8787+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
8888+ #
8989+ # We also recommend setting `force_ssl` in your config/prod.exs,
9090+ # ensuring no data is ever sent via http, always redirecting to https:
9191+ #
9292+ # config :statusphere_elixir, StatusphereElixirWeb.Endpoint,
9393+ # force_ssl: [hsts: true]
9494+ #
9595+ # Check `Plug.SSL` for all available options in `force_ssl`.
9696+end
+32
config/test.exs
···11+import Config
22+33+# Configure your database
44+#
55+# The MIX_TEST_PARTITION environment variable can be used
66+# to provide built-in test partitioning in CI environment.
77+# Run `mix help test` for more information.
88+config :statusphere_elixir, StatusphereElixir.Repo,
99+ database: Path.expand("../statusphere_elixir_test.db", __DIR__),
1010+ pool_size: 5,
1111+ pool: Ecto.Adapters.SQL.Sandbox
1212+1313+# We don't run a server during test. If one is required,
1414+# you can enable the server option below.
1515+config :statusphere_elixir, StatusphereElixirWeb.Endpoint,
1616+ http: [ip: {127, 0, 0, 1}, port: 4002],
1717+ secret_key_base: "AjDaXXlcrR04FEL+g7EV1+pKX7Fnvuhp63uWWK4I8zwKYmgReKy4WxqvdSNDdoT7",
1818+ server: false
1919+2020+# Print only warnings and errors during test
2121+config :logger, level: :warning
2222+2323+# Initialize plugs at runtime for faster test compilation
2424+config :phoenix, :plug_init_mode, :runtime
2525+2626+# Enable helpful, but potentially expensive runtime checks
2727+config :phoenix_live_view,
2828+ enable_expensive_runtime_checks: true
2929+3030+# Sort query params output of verified routes for robust url comparisons
3131+config :phoenix,
3232+ sort_verified_routes_query_params: true
···11+defmodule StatusphereElixir do
22+ @moduledoc """
33+ StatusphereElixir keeps the contexts that define your domain
44+ and business logic.
55+66+ Contexts are also responsible for managing your data, regardless
77+ if it comes from the database, an external API or others.
88+ """
99+end
+41
lib/statusphere_elixir/application.ex
···11+defmodule StatusphereElixir.Application do
22+ # See https://hexdocs.pm/elixir/Application.html
33+ # for more information on OTP Applications
44+ @moduledoc false
55+66+ use Application
77+88+ @impl true
99+ def start(_type, _args) do
1010+ children = [
1111+ StatusphereElixirWeb.Telemetry,
1212+ StatusphereElixir.Repo,
1313+ {Ecto.Migrator,
1414+ repos: Application.fetch_env!(:statusphere_elixir, :ecto_repos), skip: skip_migrations?()},
1515+ {DNSCluster, query: Application.get_env(:statusphere_elixir, :dns_cluster_query) || :ignore},
1616+ {Phoenix.PubSub, name: StatusphereElixir.PubSub},
1717+ # Start a worker by calling: StatusphereElixir.Worker.start_link(arg)
1818+ # {StatusphereElixir.Worker, arg},
1919+ # Start to serve requests, typically the last entry
2020+ StatusphereElixirWeb.Endpoint
2121+ ]
2222+2323+ # See https://hexdocs.pm/elixir/Supervisor.html
2424+ # for other strategies and supported options
2525+ opts = [strategy: :one_for_one, name: StatusphereElixir.Supervisor]
2626+ Supervisor.start_link(children, opts)
2727+ end
2828+2929+ # Tell Phoenix to update the endpoint configuration
3030+ # whenever the application is updated.
3131+ @impl true
3232+ def config_change(changed, _new, removed) do
3333+ StatusphereElixirWeb.Endpoint.config_change(changed, removed)
3434+ :ok
3535+ end
3636+3737+ defp skip_migrations?() do
3838+ # By default, sqlite migrations are run when using a release
3939+ System.get_env("RELEASE_NAME") == nil
4040+ end
4141+end
+5
lib/statusphere_elixir/repo.ex
···11+defmodule StatusphereElixir.Repo do
22+ use Ecto.Repo,
33+ otp_app: :statusphere_elixir,
44+ adapter: Ecto.Adapters.SQLite3
55+end
+109
lib/statusphere_elixir_web.ex
···11+defmodule StatusphereElixirWeb do
22+ @moduledoc """
33+ The entrypoint for defining your web interface, such
44+ as controllers, components, channels, and so on.
55+66+ This can be used in your application as:
77+88+ use StatusphereElixirWeb, :controller
99+ use StatusphereElixirWeb, :html
1010+1111+ The definitions below will be executed for every controller,
1212+ component, etc, so keep them short and clean, focused
1313+ on imports, uses and aliases.
1414+1515+ Do NOT define functions inside the quoted expressions
1616+ below. Instead, define additional modules and import
1717+ those modules here.
1818+ """
1919+2020+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
2121+2222+ def router do
2323+ quote do
2424+ use Phoenix.Router, helpers: false
2525+2626+ # Import common connection and controller functions to use in pipelines
2727+ import Plug.Conn
2828+ import Phoenix.Controller
2929+ import Phoenix.LiveView.Router
3030+ end
3131+ end
3232+3333+ def channel do
3434+ quote do
3535+ use Phoenix.Channel
3636+ end
3737+ end
3838+3939+ def controller do
4040+ quote do
4141+ use Phoenix.Controller, formats: [:html, :json]
4242+4343+ import Plug.Conn
4444+4545+ unquote(verified_routes())
4646+ end
4747+ end
4848+4949+ def live_view do
5050+ quote do
5151+ use Phoenix.LiveView
5252+5353+ unquote(html_helpers())
5454+ end
5555+ end
5656+5757+ def live_component do
5858+ quote do
5959+ use Phoenix.LiveComponent
6060+6161+ unquote(html_helpers())
6262+ end
6363+ end
6464+6565+ def html do
6666+ quote do
6767+ use Phoenix.Component
6868+6969+ # Import convenience functions from controllers
7070+ import Phoenix.Controller,
7171+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
7272+7373+ # Include general helpers for rendering HTML
7474+ unquote(html_helpers())
7575+ end
7676+ end
7777+7878+ defp html_helpers do
7979+ quote do
8080+ # HTML escaping functionality
8181+ import Phoenix.HTML
8282+ # Core UI components
8383+ import StatusphereElixirWeb.CoreComponents
8484+8585+ # Common modules used in templates
8686+ alias Phoenix.LiveView.JS
8787+ alias StatusphereElixirWeb.Layouts
8888+8989+ # Routes generation with the ~p sigil
9090+ unquote(verified_routes())
9191+ end
9292+ end
9393+9494+ def verified_routes do
9595+ quote do
9696+ use Phoenix.VerifiedRoutes,
9797+ endpoint: StatusphereElixirWeb.Endpoint,
9898+ router: StatusphereElixirWeb.Router,
9999+ statics: StatusphereElixirWeb.static_paths()
100100+ end
101101+ end
102102+103103+ @doc """
104104+ When used, dispatch to the appropriate controller/live_view/etc.
105105+ """
106106+ defmacro __using__(which) when is_atom(which) do
107107+ apply(__MODULE__, which, [])
108108+ end
109109+end
···11+defmodule StatusphereElixirWeb.ErrorHTML do
22+ @moduledoc """
33+ This module is invoked by your endpoint in case of errors on HTML requests.
44+55+ See config/config.exs.
66+ """
77+ use StatusphereElixirWeb, :html
88+99+ # If you want to customize your error pages,
1010+ # uncomment the embed_templates/1 call below
1111+ # and add pages to the error directory:
1212+ #
1313+ # * lib/statusphere_elixir_web/controllers/error_html/404.html.heex
1414+ # * lib/statusphere_elixir_web/controllers/error_html/500.html.heex
1515+ #
1616+ # embed_templates "error_html/*"
1717+1818+ # The default is to render a plain text page based on
1919+ # the template name. For example, "404.html" becomes
2020+ # "Not Found".
2121+ def render(template, _assigns) do
2222+ Phoenix.Controller.status_message_from_template(template)
2323+ end
2424+end
···11+defmodule StatusphereElixirWeb.ErrorJSON do
22+ @moduledoc """
33+ This module is invoked by your endpoint in case of errors on JSON requests.
44+55+ See config/config.exs.
66+ """
77+88+ # If you want to customize a particular status code,
99+ # you may add your own clauses, such as:
1010+ #
1111+ # def render("500.json", _assigns) do
1212+ # %{errors: %{detail: "Internal Server Error"}}
1313+ # end
1414+1515+ # By default, Phoenix returns the status message from
1616+ # the template name. For example, "404.json" becomes
1717+ # "Not Found".
1818+ def render(template, _assigns) do
1919+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
2020+ end
2121+end
···11+defmodule StatusphereElixirWeb.PageController do
22+ use StatusphereElixirWeb, :controller
33+44+ def home(conn, _params) do
55+ render(conn, :home)
66+ end
77+end
···11+defmodule StatusphereElixirWeb.PageHTML do
22+ @moduledoc """
33+ This module contains pages rendered by PageController.
44+55+ See the `page_html` directory for all templates available.
66+ """
77+ use StatusphereElixirWeb, :html
88+99+ embed_templates "page_html/*"
1010+end
···11+defmodule StatusphereElixirWeb.Endpoint do
22+ use Phoenix.Endpoint, otp_app: :statusphere_elixir
33+44+ # The session will be stored in the cookie and signed,
55+ # this means its contents can be read but not tampered with.
66+ # Set :encryption_salt if you would also like to encrypt it.
77+ @session_options [
88+ store: :cookie,
99+ key: "_statusphere_elixir_key",
1010+ signing_salt: "zbKtWcZI",
1111+ same_site: "Lax"
1212+ ]
1313+1414+ socket "/live", Phoenix.LiveView.Socket,
1515+ websocket: [connect_info: [session: @session_options]],
1616+ longpoll: [connect_info: [session: @session_options]]
1717+1818+ # Serve at "/" the static files from "priv/static" directory.
1919+ #
2020+ # When code reloading is disabled (e.g., in production),
2121+ # the `gzip` option is enabled to serve compressed
2222+ # static files generated by running `phx.digest`.
2323+ plug Plug.Static,
2424+ at: "/",
2525+ from: :statusphere_elixir,
2626+ gzip: not code_reloading?,
2727+ only: StatusphereElixirWeb.static_paths(),
2828+ raise_on_missing_only: code_reloading?
2929+3030+ # Code reloading can be explicitly enabled under the
3131+ # :code_reloader configuration of your endpoint.
3232+ if code_reloading? do
3333+ socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
3434+ plug Phoenix.LiveReloader
3535+ plug Phoenix.CodeReloader
3636+ plug Phoenix.Ecto.CheckRepoStatus, otp_app: :statusphere_elixir
3737+ end
3838+3939+ plug Phoenix.LiveDashboard.RequestLogger,
4040+ param_key: "request_logger",
4141+ cookie_key: "request_logger"
4242+4343+ plug Plug.RequestId
4444+ plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
4545+4646+ plug Plug.Parsers,
4747+ parsers: [:urlencoded, :multipart, :json],
4848+ pass: ["*/*"],
4949+ json_decoder: Phoenix.json_library()
5050+5151+ plug Plug.MethodOverride
5252+ plug Plug.Head
5353+ plug Plug.Session, @session_options
5454+ plug StatusphereElixirWeb.Router
5555+end
+43
lib/statusphere_elixir_web/router.ex
···11+defmodule StatusphereElixirWeb.Router do
22+ use StatusphereElixirWeb, :router
33+44+ pipeline :browser do
55+ plug :accepts, ["html"]
66+ plug :fetch_session
77+ plug :fetch_live_flash
88+ plug :put_root_layout, html: {StatusphereElixirWeb.Layouts, :root}
99+ plug :protect_from_forgery
1010+ plug :put_secure_browser_headers
1111+ end
1212+1313+ pipeline :api do
1414+ plug :accepts, ["json"]
1515+ end
1616+1717+ scope "/", StatusphereElixirWeb do
1818+ pipe_through :browser
1919+2020+ get "/", PageController, :home
2121+ end
2222+2323+ # Other scopes may use custom stacks.
2424+ # scope "/api", StatusphereElixirWeb do
2525+ # pipe_through :api
2626+ # end
2727+2828+ # Enable LiveDashboard in development
2929+ if Application.compile_env(:statusphere_elixir, :dev_routes) do
3030+ # If you want to use the LiveDashboard in production, you should put
3131+ # it behind authentication and allow only admins to access it.
3232+ # If your application does not have an admins-only section yet,
3333+ # you can use Plug.BasicAuth to set up some basic authentication
3434+ # as long as you are also using SSL (which you should anyway).
3535+ import Phoenix.LiveDashboard.Router
3636+3737+ scope "/dev" do
3838+ pipe_through :browser
3939+4040+ live_dashboard "/dashboard", metrics: StatusphereElixirWeb.Telemetry
4141+ end
4242+ end
4343+end
+93
lib/statusphere_elixir_web/telemetry.ex
···11+defmodule StatusphereElixirWeb.Telemetry do
22+ use Supervisor
33+ import Telemetry.Metrics
44+55+ def start_link(arg) do
66+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
77+ end
88+99+ @impl true
1010+ def init(_arg) do
1111+ children = [
1212+ # Telemetry poller will execute the given period measurements
1313+ # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
1414+ {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
1515+ # Add reporters as children of your supervision tree.
1616+ # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
1717+ ]
1818+1919+ Supervisor.init(children, strategy: :one_for_one)
2020+ end
2121+2222+ def metrics do
2323+ [
2424+ # Phoenix Metrics
2525+ summary("phoenix.endpoint.start.system_time",
2626+ unit: {:native, :millisecond}
2727+ ),
2828+ summary("phoenix.endpoint.stop.duration",
2929+ unit: {:native, :millisecond}
3030+ ),
3131+ summary("phoenix.router_dispatch.start.system_time",
3232+ tags: [:route],
3333+ unit: {:native, :millisecond}
3434+ ),
3535+ summary("phoenix.router_dispatch.exception.duration",
3636+ tags: [:route],
3737+ unit: {:native, :millisecond}
3838+ ),
3939+ summary("phoenix.router_dispatch.stop.duration",
4040+ tags: [:route],
4141+ unit: {:native, :millisecond}
4242+ ),
4343+ summary("phoenix.socket_connected.duration",
4444+ unit: {:native, :millisecond}
4545+ ),
4646+ sum("phoenix.socket_drain.count"),
4747+ summary("phoenix.channel_joined.duration",
4848+ unit: {:native, :millisecond}
4949+ ),
5050+ summary("phoenix.channel_handled_in.duration",
5151+ tags: [:event],
5252+ unit: {:native, :millisecond}
5353+ ),
5454+5555+ # Database Metrics
5656+ summary("statusphere_elixir.repo.query.total_time",
5757+ unit: {:native, :millisecond},
5858+ description: "The sum of the other measurements"
5959+ ),
6060+ summary("statusphere_elixir.repo.query.decode_time",
6161+ unit: {:native, :millisecond},
6262+ description: "The time spent decoding the data received from the database"
6363+ ),
6464+ summary("statusphere_elixir.repo.query.query_time",
6565+ unit: {:native, :millisecond},
6666+ description: "The time spent executing the query"
6767+ ),
6868+ summary("statusphere_elixir.repo.query.queue_time",
6969+ unit: {:native, :millisecond},
7070+ description: "The time spent waiting for a database connection"
7171+ ),
7272+ summary("statusphere_elixir.repo.query.idle_time",
7373+ unit: {:native, :millisecond},
7474+ description:
7575+ "The time the connection spent waiting before being checked out for the query"
7676+ ),
7777+7878+ # VM Metrics
7979+ summary("vm.memory.total", unit: {:byte, :kilobyte}),
8080+ summary("vm.total_run_queue_lengths.total"),
8181+ summary("vm.total_run_queue_lengths.cpu"),
8282+ summary("vm.total_run_queue_lengths.io")
8383+ ]
8484+ end
8585+8686+ defp periodic_measurements do
8787+ [
8888+ # A module, function and arguments to be invoked periodically.
8989+ # This function must call :telemetry.execute/3 and a metric must be added above.
9090+ # {StatusphereElixirWeb, :count_users, []}
9191+ ]
9292+ end
9393+end
+97
mix.exs
···11+defmodule StatusphereElixir.MixProject do
22+ use Mix.Project
33+44+ def project do
55+ [
66+ app: :statusphere_elixir,
77+ version: "0.1.0",
88+ elixir: "~> 1.15",
99+ elixirc_paths: elixirc_paths(Mix.env()),
1010+ start_permanent: Mix.env() == :prod,
1111+ aliases: aliases(),
1212+ deps: deps(),
1313+ compilers: [:phoenix_live_view] ++ Mix.compilers(),
1414+ listeners: [Phoenix.CodeReloader]
1515+ ]
1616+ end
1717+1818+ # Configuration for the OTP application.
1919+ #
2020+ # Type `mix help compile.app` for more information.
2121+ def application do
2222+ [
2323+ mod: {StatusphereElixir.Application, []},
2424+ extra_applications: [:logger, :runtime_tools]
2525+ ]
2626+ end
2727+2828+ def cli do
2929+ [
3030+ preferred_envs: [precommit: :test]
3131+ ]
3232+ end
3333+3434+ # Specifies which paths to compile per environment.
3535+ defp elixirc_paths(:test), do: ["lib", "test/support"]
3636+ defp elixirc_paths(_), do: ["lib"]
3737+3838+ # Specifies your project dependencies.
3939+ #
4040+ # Type `mix help deps` for examples and options.
4141+ defp deps do
4242+ [
4343+ {:phoenix, "~> 1.8.3"},
4444+ {:phoenix_ecto, "~> 4.5"},
4545+ {:ecto_sql, "~> 3.13"},
4646+ {:ecto_sqlite3, ">= 0.0.0"},
4747+ {:phoenix_html, "~> 4.1"},
4848+ {:phoenix_live_reload, "~> 1.2", only: :dev},
4949+ {:phoenix_live_view, "~> 1.1.0"},
5050+ {:lazy_html, ">= 0.1.0", only: :test},
5151+ {:phoenix_live_dashboard, "~> 0.8.3"},
5252+ {:esbuild, "~> 0.10", runtime: Mix.env() == :dev},
5353+ {:tailwind, "~> 0.3", runtime: Mix.env() == :dev},
5454+ {:heroicons,
5555+ github: "tailwindlabs/heroicons",
5656+ tag: "v2.2.0",
5757+ sparse: "optimized",
5858+ app: false,
5959+ compile: false,
6060+ depth: 1},
6161+ {:telemetry_metrics, "~> 1.0"},
6262+ {:telemetry_poller, "~> 1.0"},
6363+ {:jason, "~> 1.2"},
6464+ {:dns_cluster, "~> 0.2.0"},
6565+ {:bandit, "~> 1.5"},
6666+ {:atex, "~> 0.6"},
6767+ {:drinkup, "~> 0.1"},
6868+ {:typedstruct, "~> 0.5"}
6969+ ]
7070+ end
7171+7272+ # Aliases are shortcuts or tasks specific to the current project.
7373+ # For example, to install project dependencies and perform other setup tasks, run:
7474+ #
7575+ # $ mix setup
7676+ #
7777+ # See the documentation for `Mix` for more info on aliases.
7878+ defp aliases do
7979+ [
8080+ setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
8181+ "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
8282+ "ecto.reset": ["ecto.drop", "ecto.setup"],
8383+ test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
8484+ "assets.setup": [
8585+ # "tailwind.install --if-missing",
8686+ "esbuild.install --if-missing"
8787+ ],
8888+ "assets.build": ["compile", "tailwind statusphere_elixir", "esbuild statusphere_elixir"],
8989+ "assets.deploy": [
9090+ "tailwind statusphere_elixir --minify",
9191+ "esbuild statusphere_elixir --minify",
9292+ "phx.digest"
9393+ ],
9494+ precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"]
9595+ ]
9696+ end
9797+end
···11+# Script for populating the database. You can run it as:
22+#
33+# mix run priv/repo/seeds.exs
44+#
55+# Inside the script, you can read and write to any of your
66+# repositories directly:
77+#
88+# StatusphereElixir.Repo.insert!(%StatusphereElixir.SomeSchema{})
99+#
1010+# We recommend using the bang functions (`insert!`, `update!`
1111+# and so on) as they will fail if something goes wrong.
···11+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
22+#
33+# To ban all spiders from the entire site uncomment the next two lines:
44+# User-agent: *
55+# Disallow: /
···11+defmodule StatusphereElixirWeb.ErrorJSONTest do
22+ use StatusphereElixirWeb.ConnCase, async: true
33+44+ test "renders 404" do
55+ assert StatusphereElixirWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
66+ end
77+88+ test "renders 500" do
99+ assert StatusphereElixirWeb.ErrorJSON.render("500.json", %{}) ==
1010+ %{errors: %{detail: "Internal Server Error"}}
1111+ end
1212+end
···11+defmodule StatusphereElixirWeb.PageControllerTest do
22+ use StatusphereElixirWeb.ConnCase
33+44+ test "GET /", %{conn: conn} do
55+ conn = get(conn, ~p"/")
66+ assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
77+ end
88+end
+38
test/support/conn_case.ex
···11+defmodule StatusphereElixirWeb.ConnCase do
22+ @moduledoc """
33+ This module defines the test case to be used by
44+ tests that require setting up a connection.
55+66+ Such tests rely on `Phoenix.ConnTest` and also
77+ import other functionality to make it easier
88+ to build common data structures and query the data layer.
99+1010+ Finally, if the test case interacts with the database,
1111+ we enable the SQL sandbox, so changes done to the database
1212+ are reverted at the end of every test. If you are using
1313+ PostgreSQL, you can even run database tests asynchronously
1414+ by setting `use StatusphereElixirWeb.ConnCase, async: true`, although
1515+ this option is not recommended for other databases.
1616+ """
1717+1818+ use ExUnit.CaseTemplate
1919+2020+ using do
2121+ quote do
2222+ # The default endpoint for testing
2323+ @endpoint StatusphereElixirWeb.Endpoint
2424+2525+ use StatusphereElixirWeb, :verified_routes
2626+2727+ # Import conveniences for testing with connections
2828+ import Plug.Conn
2929+ import Phoenix.ConnTest
3030+ import StatusphereElixirWeb.ConnCase
3131+ end
3232+ end
3333+3434+ setup tags do
3535+ StatusphereElixir.DataCase.setup_sandbox(tags)
3636+ {:ok, conn: Phoenix.ConnTest.build_conn()}
3737+ end
3838+end
+58
test/support/data_case.ex
···11+defmodule StatusphereElixir.DataCase do
22+ @moduledoc """
33+ This module defines the setup for tests requiring
44+ access to the application's data layer.
55+66+ You may define functions here to be used as helpers in
77+ your tests.
88+99+ Finally, if the test case interacts with the database,
1010+ we enable the SQL sandbox, so changes done to the database
1111+ are reverted at the end of every test. If you are using
1212+ PostgreSQL, you can even run database tests asynchronously
1313+ by setting `use StatusphereElixir.DataCase, async: true`, although
1414+ this option is not recommended for other databases.
1515+ """
1616+1717+ use ExUnit.CaseTemplate
1818+1919+ using do
2020+ quote do
2121+ alias StatusphereElixir.Repo
2222+2323+ import Ecto
2424+ import Ecto.Changeset
2525+ import Ecto.Query
2626+ import StatusphereElixir.DataCase
2727+ end
2828+ end
2929+3030+ setup tags do
3131+ StatusphereElixir.DataCase.setup_sandbox(tags)
3232+ :ok
3333+ end
3434+3535+ @doc """
3636+ Sets up the sandbox based on the test tags.
3737+ """
3838+ def setup_sandbox(tags) do
3939+ pid = Ecto.Adapters.SQL.Sandbox.start_owner!(StatusphereElixir.Repo, shared: not tags[:async])
4040+ on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
4141+ end
4242+4343+ @doc """
4444+ A helper that transforms changeset errors into a map of messages.
4545+4646+ assert {:error, changeset} = Accounts.create_user(%{password: "short"})
4747+ assert "password is too short" in errors_on(changeset).password
4848+ assert %{password: ["password is too short"]} = errors_on(changeset)
4949+5050+ """
5151+ def errors_on(changeset) do
5252+ Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
5353+ Regex.replace(~r"%{(\w+)}", message, fn _, key ->
5454+ opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
5555+ end)
5656+ end)
5757+ end
5858+end