Initialize Phoenix 1.8 application with latest dependencies.

96faa2ff3843 · AtlantisPleb · · parent fb5dd1872dcf

Initialize Phoenix 1.8 application with latest dependencies.

Generated with `phx_new` 1.8.9 and resolved to current latest Hex
versions, including Phoenix 1.8.11, LiveView 1.2.9, Ecto 3.14.2,
Postgrex 0.22.4, and Bandit 1.12.4. The existing README is preserved.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • added .formatter.exs
  • added .gitignore
  • added assets/css/app.css
  • added assets/js/app.js
  • added assets/tsconfig.json
  • added assets/vendor/heroicons.js
  • added assets/vendor/topbar.js
  • added config/config.exs
  • added config/dev.exs
  • added config/prod.exs
  • added config/runtime.exs
  • added config/test.exs
  • added lib/openagents.ex
  • added lib/openagents/application.ex
  • added lib/openagents/mailer.ex
  • added lib/openagents/repo.ex
  • added lib/openagents_web.ex
  • added lib/openagents_web/components/core_components.ex
  • added lib/openagents_web/components/layouts.ex
  • added lib/openagents_web/components/layouts/root.html.heex
  • added lib/openagents_web/controllers/error_html.ex
  • added lib/openagents_web/controllers/error_json.ex
  • added lib/openagents_web/controllers/page_controller.ex
  • added lib/openagents_web/controllers/page_html.ex
  • added lib/openagents_web/controllers/page_html/home.html.heex
  • added lib/openagents_web/endpoint.ex
  • added lib/openagents_web/gettext.ex
  • added lib/openagents_web/router.ex
  • added lib/openagents_web/telemetry.ex
  • added mix.exs
  • added mix.lock
  • added priv/gettext/en/LC_MESSAGES/errors.po
  • added priv/gettext/errors.pot
  • added priv/repo/migrations/.formatter.exs
  • added priv/repo/seeds.exs
  • added priv/static/favicon.ico
  • added priv/static/images/logo.svg
  • added priv/static/robots.txt
  • added test/openagents_web/controllers/error_html_test.exs
  • added test/openagents_web/controllers/error_json_test.exs
  • added test/openagents_web/controllers/page_controller_test.exs
  • added test/support/conn_case.ex
  • added test/support/data_case.ex
  • added test/test_helper.exs

Diff

44 files changed, +2681 -0

.formatter.exs added +6

@@ -0,0 +1,6 @@

1
[
2
  import_deps: [:ecto, :ecto_sql, :phoenix],
3
  subdirectories: ["priv/*/migrations"],
4
  plugins: [Phoenix.LiveView.HTMLFormatter],
5
  inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
6
]
.gitignore added +37

@@ -0,0 +1,37 @@

1
# The directory Mix will write compiled artifacts to.
2
/_build/
3
4
# If you run "mix test --cover", coverage assets end up here.
5
/cover/
6
7
# The directory Mix downloads your dependencies sources to.
8
/deps/
9
10
# Where 3rd-party dependencies like ExDoc output generated docs.
11
/doc/
12
13
# Ignore .fetch files in case you like to edit your project deps locally.
14
/.fetch
15
16
# If the VM crashes, it generates a dump, let's ignore it too.
17
erl_crash.dump
18
19
# Also ignore archive artifacts (built via "mix archive.build").
20
*.ez
21
22
# Temporary files, for example, from tests.
23
/tmp/
24
25
# Ignore package tarball (built via "mix hex.build").
26
openagents-*.tar
27
28
# Ignore assets that are produced by build tools.
29
/priv/static/assets/
30
31
# Ignore digested assets cache.
32
/priv/static/cache_manifest.json
33
34
# In case you use Node.js/npm, you want to ignore these.
35
npm-debug.log
36
/assets/node_modules/
37
assets/css/app.css added +105

@@ -0,0 +1,105 @@

1
/* See the Tailwind configuration guide for advanced usage
2
   https://tailwindcss.com/docs/configuration */
3
4
@import "tailwindcss" source(none);
5
@import "phoenix-colocated/openagents/colocated.css";
6
@source "../css";
7
@source "../js";
8
@source "../../lib/openagents_web";
9
/* Required for Tailwind to automatically pick up changes in colocated CSS files in dev */
10
@source "../../_build/dev/phoenix-colocated/openagents/*/";
11
12
/* A Tailwind plugin that makes "hero-#{ICON}" classes available.
13
   The heroicons installation itself is managed by your mix.exs */
14
@plugin "../vendor/heroicons";
15
16
/* daisyUI Tailwind Plugin. */
17
@plugin "daisyui/packages/bundle/daisyui" {
18
  themes: false;
19
}
20
21
/* daisyUI theme plugin.
22
  We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired
23
  on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */
24
@plugin "daisyui/packages/bundle/daisyui-theme" {
25
  name: "dark";
26
  default: false;
27
  prefersdark: true;
28
  color-scheme: "dark";
29
  --color-base-100: oklch(30.33% 0.016 252.42);
30
  --color-base-200: oklch(25.26% 0.014 253.1);
31
  --color-base-300: oklch(20.15% 0.012 254.09);
32
  --color-base-content: oklch(97.807% 0.029 256.847);
33
  --color-primary: oklch(58% 0.233 277.117);
34
  --color-primary-content: oklch(96% 0.018 272.314);
35
  --color-secondary: oklch(58% 0.233 277.117);
36
  --color-secondary-content: oklch(96% 0.018 272.314);
37
  --color-accent: oklch(60% 0.25 292.717);
38
  --color-accent-content: oklch(96% 0.016 293.756);
39
  --color-neutral: oklch(37% 0.044 257.287);
40
  --color-neutral-content: oklch(98% 0.003 247.858);
41
  --color-info: oklch(58% 0.158 241.966);
42
  --color-info-content: oklch(97% 0.013 236.62);
43
  --color-success: oklch(60% 0.118 184.704);
44
  --color-success-content: oklch(98% 0.014 180.72);
45
  --color-warning: oklch(66% 0.179 58.318);
46
  --color-warning-content: oklch(98% 0.022 95.277);
47
  --color-error: oklch(58% 0.253 17.585);
48
  --color-error-content: oklch(96% 0.015 12.422);
49
  --radius-selector: 0.25rem;
50
  --radius-field: 0.25rem;
51
  --radius-box: 0.5rem;
52
  --size-selector: 0.21875rem;
53
  --size-field: 0.21875rem;
54
  --border: 1.5px;
55
  --depth: 1;
56
  --noise: 0;
57
}
58
59
@plugin "daisyui/packages/bundle/daisyui-theme" {
60
  name: "light";
61
  default: true;
62
  prefersdark: false;
63
  color-scheme: "light";
64
  --color-base-100: oklch(98% 0 0);
65
  --color-base-200: oklch(96% 0.001 286.375);
66
  --color-base-300: oklch(92% 0.004 286.32);
67
  --color-base-content: oklch(21% 0.006 285.885);
68
  --color-primary: oklch(70% 0.213 47.604);
69
  --color-primary-content: oklch(98% 0.016 73.684);
70
  --color-secondary: oklch(55% 0.027 264.364);
71
  --color-secondary-content: oklch(98% 0.002 247.839);
72
  --color-accent: oklch(0% 0 0);
73
  --color-accent-content: oklch(100% 0 0);
74
  --color-neutral: oklch(44% 0.017 285.786);
75
  --color-neutral-content: oklch(98% 0 0);
76
  --color-info: oklch(62% 0.214 259.815);
77
  --color-info-content: oklch(97% 0.014 254.604);
78
  --color-success: oklch(70% 0.14 182.503);
79
  --color-success-content: oklch(98% 0.014 180.72);
80
  --color-warning: oklch(66% 0.179 58.318);
81
  --color-warning-content: oklch(98% 0.022 95.277);
82
  --color-error: oklch(58% 0.253 17.585);
83
  --color-error-content: oklch(96% 0.015 12.422);
84
  --radius-selector: 0.25rem;
85
  --radius-field: 0.25rem;
86
  --radius-box: 0.5rem;
87
  --size-selector: 0.21875rem;
88
  --size-field: 0.21875rem;
89
  --border: 1.5px;
90
  --depth: 1;
91
  --noise: 0;
92
}
93
94
/* Add variants based on LiveView classes */
95
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
96
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
97
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
98
99
/* Use the data attribute for dark mode  */
100
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
101
102
/* Make LiveView wrapper divs transparent for layout */
103
[data-phx-session], [data-phx-teleported-src] { display: contents }
104
105
/* This file is for your main application CSS */
assets/js/app.js added +83

@@ -0,0 +1,83 @@

1
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
2
// to get started and then uncomment the line below.
3
// import "./user_socket.js"
4
5
// You can include dependencies in two ways.
6
//
7
// The simplest option is to put them in assets/vendor and
8
// import them using relative paths:
9
//
10
//     import "../vendor/some-package.js"
11
//
12
// Alternatively, you can `npm install some-package --prefix assets` and import
13
// them using a path starting with the package name:
14
//
15
//     import "some-package"
16
//
17
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
18
// To load it, simply add a second `<link>` to your `root.html.heex` file.
19
20
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
21
import "phoenix_html"
22
// Establish Phoenix Socket and LiveView configuration.
23
import {Socket} from "phoenix"
24
import {LiveSocket} from "phoenix_live_view"
25
import {hooks as colocatedHooks} from "phoenix-colocated/openagents"
26
import topbar from "../vendor/topbar"
27
28
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
29
const liveSocket = new LiveSocket("/live", Socket, {
30
  longPollFallbackMs: 2500,
31
  params: {_csrf_token: csrfToken},
32
  hooks: {...colocatedHooks},
33
})
34
35
// Show progress bar on live navigation and form submits
36
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
37
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
38
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
39
40
// connect if there are any LiveViews on the page
41
liveSocket.connect()
42
43
// expose liveSocket on window for web console debug logs and latency simulation:
44
// >> liveSocket.enableDebug()
45
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
46
// >> liveSocket.disableLatencySim()
47
window.liveSocket = liveSocket
48
49
// The lines below enable quality of life phoenix_live_reload
50
// development features:
51
//
52
//     1. stream server logs to the browser console
53
//     2. click on elements to jump to their definitions in your code editor
54
//
55
if (process.env.NODE_ENV === "development") {
56
  window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
57
    // Enable server log streaming to client.
58
    // Disable with reloader.disableServerLogs()
59
    reloader.enableServerLogs()
60
61
    // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
62
    //
63
    //   * click with "c" key pressed to open at caller location
64
    //   * click with "d" key pressed to open at function component definition location
65
    let keyDown
66
    window.addEventListener("keydown", e => keyDown = e.key)
67
    window.addEventListener("keyup", _e => keyDown = null)
68
    window.addEventListener("click", e => {
69
      if(keyDown === "c"){
70
        e.preventDefault()
71
        e.stopImmediatePropagation()
72
        reloader.openEditorAtCaller(e.target)
73
      } else if(keyDown === "d"){
74
        e.preventDefault()
75
        e.stopImmediatePropagation()
76
        reloader.openEditorAtDef(e.target)
77
      }
78
    }, true)
79
80
    window.liveReloader = reloader
81
  })
82
}
83
assets/tsconfig.json added +32

@@ -0,0 +1,32 @@

1
// This file is needed on most editors to enable the intelligent autocompletion
2
// of LiveView's JavaScript API methods. You can safely delete it if you don't need it.
3
//
4
// Note: This file assumes a basic esbuild setup without node_modules.
5
// We include a generic paths alias to deps to mimic how esbuild resolves
6
// the Phoenix and LiveView JavaScript assets.
7
// If you have a package.json in your project, you should remove the
8
// paths configuration and instead add the phoenix dependencies to the
9
// dependencies section of your package.json:
10
//
11
// {
12
//   ...
13
//   "dependencies": {
14
//     ...,
15
//     "phoenix": "../deps/phoenix",
16
//     "phoenix_html": "../deps/phoenix_html",
17
//     "phoenix_live_view": "../deps/phoenix_live_view"
18
//   }
19
// }
20
//
21
// Feel free to adjust this configuration however you need.
22
{
23
  "compilerOptions": {
24
    "baseUrl": ".",
25
    "paths": {
26
      "*": ["../deps/*"]
27
    },
28
    "allowJs": true,
29
    "noEmit": true
30
  },
31
  "include": ["js/**/*"]
32
}
assets/vendor/heroicons.js added +43

@@ -0,0 +1,43 @@

1
const plugin = require("tailwindcss/plugin")
2
const fs = require("fs")
3
const path = require("path")
4
5
module.exports = plugin(function({matchComponents, theme}) {
6
  let iconsDir = path.join(__dirname, "../../deps/heroicons/optimized")
7
  let values = {}
8
  let icons = [
9
    ["", "/24/outline"],
10
    ["-solid", "/24/solid"],
11
    ["-mini", "/20/solid"],
12
    ["-micro", "/16/solid"]
13
  ]
14
  icons.forEach(([suffix, dir]) => {
15
    fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
16
      let name = path.basename(file, ".svg") + suffix
17
      values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
18
    })
19
  })
20
  matchComponents({
21
    "hero": ({name, fullPath}) => {
22
      let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
23
      content = encodeURIComponent(content)
24
      let size = theme("spacing.6")
25
      if (name.endsWith("-mini")) {
26
        size = theme("spacing.5")
27
      } else if (name.endsWith("-micro")) {
28
        size = theme("spacing.4")
29
      }
30
      return {
31
        [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
32
        "-webkit-mask": `var(--hero-${name})`,
33
        "mask": `var(--hero-${name})`,
34
        "mask-repeat": "no-repeat",
35
        "background-color": "currentColor",
36
        "vertical-align": "middle",
37
        "display": "inline-block",
38
        "width": size,
39
        "height": size
40
      }
41
    }
42
  }, {values})
43
})
assets/vendor/topbar.js added +138

@@ -0,0 +1,138 @@

1
/**
2
 * @license MIT
3
 * topbar 3.0.0
4
 * http://buunguyen.github.io/topbar
5
 * Copyright (c) 2024 Buu Nguyen
6
 */
7
(function (window, document) {
8
  "use strict";
9
10
  var canvas,
11
    currentProgress,
12
    showing,
13
    progressTimerId = null,
14
    fadeTimerId = null,
15
    delayTimerId = null,
16
    addEvent = function (elem, type, handler) {
17
      if (elem.addEventListener) elem.addEventListener(type, handler, false);
18
      else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
19
      else elem["on" + type] = handler;
20
    },
21
    options = {
22
      autoRun: true,
23
      barThickness: 3,
24
      barColors: {
25
        0: "rgba(26,  188, 156, .9)",
26
        ".25": "rgba(52,  152, 219, .9)",
27
        ".50": "rgba(241, 196, 15,  .9)",
28
        ".75": "rgba(230, 126, 34,  .9)",
29
        "1.0": "rgba(211, 84,  0,   .9)",
30
      },
31
      shadowBlur: 10,
32
      shadowColor: "rgba(0,   0,   0,   .6)",
33
      className: null,
34
    },
35
    repaint = function () {
36
      canvas.width = window.innerWidth;
37
      canvas.height = options.barThickness * 5; // need space for shadow
38
39
      var ctx = canvas.getContext("2d");
40
      ctx.shadowBlur = options.shadowBlur;
41
      ctx.shadowColor = options.shadowColor;
42
43
      var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
44
      for (var stop in options.barColors)
45
        lineGradient.addColorStop(stop, options.barColors[stop]);
46
      ctx.lineWidth = options.barThickness;
47
      ctx.beginPath();
48
      ctx.moveTo(0, options.barThickness / 2);
49
      ctx.lineTo(
50
        Math.ceil(currentProgress * canvas.width),
51
        options.barThickness / 2
52
      );
53
      ctx.strokeStyle = lineGradient;
54
      ctx.stroke();
55
    },
56
    createCanvas = function () {
57
      canvas = document.createElement("canvas");
58
      var style = canvas.style;
59
      style.position = "fixed";
60
      style.top = style.left = style.right = style.margin = style.padding = 0;
61
      style.zIndex = 100001;
62
      style.display = "none";
63
      if (options.className) canvas.classList.add(options.className);
64
      addEvent(window, "resize", repaint);
65
    },
66
    topbar = {
67
      config: function (opts) {
68
        for (var key in opts)
69
          if (options.hasOwnProperty(key)) options[key] = opts[key];
70
      },
71
      show: function (delay) {
72
        if (showing) return;
73
        if (delay) {
74
          if (delayTimerId) return;
75
          delayTimerId = setTimeout(() => topbar.show(), delay);
76
        } else {
77
          showing = true;
78
          if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
79
          if (!canvas) createCanvas();
80
          if (!canvas.parentElement) document.body.appendChild(canvas);
81
          canvas.style.opacity = 1;
82
          canvas.style.display = "block";
83
          topbar.progress(0);
84
          if (options.autoRun) {
85
            (function loop() {
86
              progressTimerId = window.requestAnimationFrame(loop);
87
              topbar.progress(
88
                "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
89
              );
90
            })();
91
          }
92
        }
93
      },
94
      progress: function (to) {
95
        if (typeof to === "undefined") return currentProgress;
96
        if (typeof to === "string") {
97
          to =
98
            (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
99
              ? currentProgress
100
              : 0) + parseFloat(to);
101
        }
102
        currentProgress = to > 1 ? 1 : to;
103
        repaint();
104
        return currentProgress;
105
      },
106
      hide: function () {
107
        clearTimeout(delayTimerId);
108
        delayTimerId = null;
109
        if (!showing) return;
110
        showing = false;
111
        if (progressTimerId != null) {
112
          window.cancelAnimationFrame(progressTimerId);
113
          progressTimerId = null;
114
        }
115
        (function loop() {
116
          if (topbar.progress("+.1") >= 1) {
117
            canvas.style.opacity -= 0.05;
118
            if (canvas.style.opacity <= 0.05) {
119
              canvas.style.display = "none";
120
              fadeTimerId = null;
121
              return;
122
            }
123
          }
124
          fadeTimerId = window.requestAnimationFrame(loop);
125
        })();
126
      },
127
    };
128
129
  if (typeof module === "object" && typeof module.exports === "object") {
130
    module.exports = topbar;
131
  } else if (typeof define === "function" && define.amd) {
132
    define(function () {
133
      return topbar;
134
    });
135
  } else {
136
    this.topbar = topbar;
137
  }
138
}.call(this, window, document));
config/config.exs added +72

@@ -0,0 +1,72 @@

1
# This file is responsible for configuring your application
2
# and its dependencies with the aid of the Config module.
3
#
4
# This configuration file is loaded before any dependency and
5
# is restricted to this project.
6
7
# General application configuration
8
import Config
9
10
config :openagents,
11
  namespace: OpenAgents,
12
  ecto_repos: [OpenAgents.Repo],
13
  generators: [timestamp_type: :utc_datetime]
14
15
# Configure the endpoint
16
config :openagents, OpenAgentsWeb.Endpoint,
17
  url: [host: "localhost"],
18
  adapter: Bandit.PhoenixAdapter,
19
  render_errors: [
20
    formats: [html: OpenAgentsWeb.ErrorHTML, json: OpenAgentsWeb.ErrorJSON],
21
    layout: false
22
  ],
23
  pubsub_server: OpenAgents.PubSub,
24
  live_view: [signing_salt: "VjtdwdCq"]
25
26
# Configure LiveView
27
config :phoenix_live_view,
28
  # the attribute set on all root tags. Used for Phoenix.LiveView.ColocatedCSS.
29
  root_tag_attribute: "phx-r"
30
31
# Configure the mailer
32
#
33
# By default it uses the "Local" adapter which stores the emails
34
# locally. You can see the emails in your browser, at "/dev/mailbox".
35
#
36
# For production it's recommended to configure a different adapter
37
# at the `config/runtime.exs`.
38
config :openagents, OpenAgents.Mailer, adapter: Swoosh.Adapters.Local
39
40
# Configure esbuild (the version is required)
41
config :esbuild,
42
  version: "0.25.4",
43
  openagents: [
44
    args:
45
      ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
46
    cd: Path.expand("../assets", __DIR__),
47
    env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
48
  ]
49
50
# Configure tailwind (the version is required)
51
config :tailwind,
52
  version: "4.3.0",
53
  openagents: [
54
    args: ~w(
55
      --input=assets/css/app.css
56
      --output=priv/static/assets/css/app.css
57
    ),
58
    cd: Path.expand("..", __DIR__),
59
    env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
60
  ]
61
62
# Configure Elixir's Logger
63
config :logger, :default_formatter,
64
  format: "$time $metadata[$level] $message\n",
65
  metadata: [:request_id]
66
67
# Use Jason for JSON parsing in Phoenix
68
config :phoenix, :json_library, Jason
69
70
# Import environment specific config. This must remain at the bottom
71
# of this file so it overrides the configuration defined above.
72
import_config "#{config_env()}.exs"
config/dev.exs added +77

@@ -0,0 +1,77 @@

1
import Config
2
3
# Configure your database
4
config :openagents, OpenAgents.Repo,
5
  username: "postgres",
6
  password: "postgres",
7
  hostname: "localhost",
8
  database: "openagents_dev",
9
  stacktrace: true,
10
  show_sensitive_data_on_connection_error: true,
11
  pool_size: 10
12
13
# For development, we disable any cache and enable
14
# debugging and code reloading.
15
#
16
# The watchers configuration can be used to run external
17
# watchers to your application. For example, we can use it
18
# to bundle .js and .css sources.
19
config :openagents, OpenAgentsWeb.Endpoint,
20
  # Binding to loopback ipv4 address prevents access from other machines.
21
  # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
22
  http: [ip: {127, 0, 0, 1}],
23
  check_origin: false,
24
  code_reloader: true,
25
  debug_errors: true,
26
  secret_key_base: "7g8XzqhjERSKPxluld+DgJttpl7vypiQg2/6wR+5AJmONkFmEzX5pQnUsdGnH/v2",
27
  watchers: [
28
    esbuild: {Esbuild, :install_and_run, [:openagents, ~w(--sourcemap=inline --watch)]},
29
    tailwind: {Tailwind, :install_and_run, [:openagents, ~w(--watch)]}
30
  ]
31
32
# ## SSL Support
33
#
34
# In order to use HTTPS in development, a self-signed
35
# certificate can be generated by running the following
36
# Mix task:
37
#
38
#     mix phx.gen.cert
39
#
40
# Run `mix help phx.gen.cert` for more information.
41
#
42
# The `http:` config above can be replaced with:
43
#
44
#     https: [
45
#       port: 4001,
46
#       cipher_suite: :strong,
47
#       keyfile: "priv/cert/selfsigned_key.pem",
48
#       certfile: "priv/cert/selfsigned.pem"
49
#     ],
50
#
51
# If desired, both `http:` and `https:` keys can be
52
# configured to run both http and https servers on
53
# different ports.
54
55
# Enable dev routes for dashboard and mailbox
56
config :openagents, dev_routes: true
57
58
# Do not include metadata nor timestamps in development logs
59
config :logger, :default_formatter, format: "[$level] $message\n"
60
61
# Set a higher stacktrace during development. Avoid configuring such
62
# in production as building large stacktraces may be expensive.
63
config :phoenix, :stacktrace_depth, 20
64
65
# Initialize plugs at runtime for faster development compilation
66
config :phoenix, :plug_init_mode, :runtime
67
68
config :phoenix_live_view,
69
  # Include debug annotations and locations in rendered markup.
70
  # Changing this configuration will require mix clean and a full recompile.
71
  debug_heex_annotations: true,
72
  debug_attributes: true,
73
  # Enable helpful, but potentially expensive runtime checks
74
  enable_expensive_runtime_checks: true
75
76
# Disable swoosh api client as it is only required for production adapters.
77
config :swoosh, :api_client, false
config/prod.exs added +33

@@ -0,0 +1,33 @@

1
import Config
2
3
# Note we also include the path to a cache manifest
4
# containing the digested version of static files. This
5
# manifest is generated by the `mix assets.deploy` task,
6
# which you should run after static files are built and
7
# before starting your production server.
8
config :openagents, OpenAgentsWeb.Endpoint,
9
  cache_static_manifest: "priv/static/cache_manifest.json"
10
11
# Force using SSL in production. This also sets the "strict-security-transport" header,
12
# known as HSTS. If you have a health check endpoint, you may want to exclude it below.
13
# Note `:force_ssl` is required to be set at compile-time.
14
config :openagents, OpenAgentsWeb.Endpoint,
15
  force_ssl: [
16
    rewrite_on: [:x_forwarded_proto],
17
    exclude: [
18
      # paths: ["/health"],
19
      hosts: ["localhost", "127.0.0.1"]
20
    ]
21
  ]
22
23
# Configure Swoosh API Client
24
config :swoosh, api_client: Swoosh.ApiClient.Req
25
26
# Disable Swoosh Local Memory Storage
27
config :swoosh, local: false
28
29
# Do not print debug messages in production
30
config :logger, level: :info
31
32
# Runtime production configuration, including reading
33
# of environment variables, is done on config/runtime.exs.
config/runtime.exs added +137

@@ -0,0 +1,137 @@

1
import Config
2
3
# config/runtime.exs is executed for all environments, including
4
# during releases. It is executed after compilation and before the
5
# system starts, so it is typically used to load production configuration
6
# and secrets from environment variables or elsewhere. Do not define
7
# any compile-time configuration in here, as it won't be applied.
8
# The block below contains prod specific runtime configuration.
9
10
# ## Using releases
11
#
12
# If you use `mix release`, you need to explicitly enable the server
13
# by passing the PHX_SERVER=true when you start it:
14
#
15
#     PHX_SERVER=true bin/openagents start
16
#
17
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
18
# script that automatically sets the env var above.
19
if System.get_env("PHX_SERVER") do
20
  config :openagents, OpenAgentsWeb.Endpoint, server: true
21
end
22
23
config :openagents, OpenAgentsWeb.Endpoint,
24
  http: [port: String.to_integer(System.get_env("PORT", "4000"))]
25
26
if config_env() == :dev do
27
  # Reload browser tabs when matching files change.
28
  config :openagents, OpenAgentsWeb.Endpoint,
29
    live_reload: [
30
      web_console_logger: true,
31
      patterns: [
32
        # Static assets, except user uploads
33
        ~r"priv/static/(?!uploads/).*\.(js|css|png|jpeg|jpg|gif|svg)$"E,
34
        # Gettext translations
35
        ~r"priv/gettext/.*\.po$"E,
36
        # Router, Controllers, LiveViews and LiveComponents
37
        ~r"lib/openagents_web/router\.ex$"E,
38
        ~r"lib/openagents_web/(controllers|live|components)/.*\.(ex|heex)$"E
39
      ]
40
    ]
41
end
42
43
if config_env() == :prod do
44
  database_url =
45
    System.get_env("DATABASE_URL") ||
46
      raise """
47
      environment variable DATABASE_URL is missing.
48
      For example: ecto://USER:PASS@HOST/DATABASE
49
      """
50
51
  maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
52
53
  config :openagents, OpenAgents.Repo,
54
    # ssl: true,
55
    url: database_url,
56
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
57
    # For machines with several cores, consider starting multiple pools of `pool_size`
58
    # pool_count: 4,
59
    socket_options: maybe_ipv6
60
61
  # The secret key base is used to sign/encrypt cookies and other secrets.
62
  # A default value is used in config/dev.exs and config/test.exs but you
63
  # want to use a different value for prod and you most likely don't want
64
  # to check this value into version control, so we use an environment
65
  # variable instead.
66
  secret_key_base =
67
    System.get_env("SECRET_KEY_BASE") ||
68
      raise """
69
      environment variable SECRET_KEY_BASE is missing.
70
      You can generate one by calling: mix phx.gen.secret
71
      """
72
73
  host = System.get_env("PHX_HOST") || "example.com"
74
75
  config :openagents, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
76
77
  config :openagents, OpenAgentsWeb.Endpoint,
78
    url: [host: host, port: 443, scheme: "https"],
79
    http: [
80
      # Enable IPv6 and bind on all interfaces.
81
      # Set it to  {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
82
      # See the documentation on https://bandit.hexdocs.pm/Bandit.html#t:options/0
83
      # for details about using IPv6 vs IPv4 and loopback vs public addresses.
84
      ip: {0, 0, 0, 0, 0, 0, 0, 0}
85
    ],
86
    secret_key_base: secret_key_base
87
88
  # ## SSL Support
89
  #
90
  # To get SSL working, you will need to add the `https` key
91
  # to your endpoint configuration:
92
  #
93
  #     config :openagents, OpenAgentsWeb.Endpoint,
94
  #       https: [
95
  #         ...,
96
  #         port: 443,
97
  #         cipher_suite: :strong,
98
  #         keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
99
  #         certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
100
  #       ]
101
  #
102
  # The `cipher_suite` is set to `:strong` to support only the
103
  # latest and more secure SSL ciphers. This means old browsers
104
  # and clients may not be supported. You can set it to
105
  # `:compatible` for wider support.
106
  #
107
  # `:keyfile` and `:certfile` expect an absolute path to the key
108
  # and cert in disk or a relative path inside priv, for example
109
  # "priv/ssl/server.key". For all supported SSL configuration
110
  # options, see https://plug.hexdocs.pm/Plug.SSL.html#configure/1
111
  #
112
  # We also recommend setting `force_ssl` in your config/prod.exs,
113
  # ensuring no data is ever sent via http, always redirecting to https:
114
  #
115
  #     config :openagents, OpenAgentsWeb.Endpoint,
116
  #       force_ssl: [hsts: true]
117
  #
118
  # Check `Plug.SSL` for all available options in `force_ssl`.
119
120
  # ## Configuring the mailer
121
  #
122
  # In production you need to configure the mailer to use a different adapter.
123
  # Here is an example configuration for Mailgun:
124
  #
125
  #     config :openagents, OpenAgents.Mailer,
126
  #       adapter: Swoosh.Adapters.Mailgun,
127
  #       api_key: System.get_env("MAILGUN_API_KEY"),
128
  #       domain: System.get_env("MAILGUN_DOMAIN")
129
  #
130
  # Most non-SMTP adapters require an API client. Swoosh supports Req, Hackney,
131
  # and Finch out-of-the-box. This configuration is typically done at
132
  # compile-time in your config/prod.exs:
133
  #
134
  #     config :swoosh, :api_client, Swoosh.ApiClient.Req
135
  #
136
  # See https://swoosh.hexdocs.pm/Swoosh.html#module-installation for details.
137
end
config/test.exs added +41

@@ -0,0 +1,41 @@

1
import Config
2
3
# Configure your database
4
#
5
# The MIX_TEST_PARTITION environment variable can be used
6
# to provide built-in test partitioning in CI environment.
7
# Run `mix help test` for more information.
8
config :openagents, OpenAgents.Repo,
9
  username: "postgres",
10
  password: "postgres",
11
  hostname: "localhost",
12
  database: "openagents_test#{System.get_env("MIX_TEST_PARTITION")}",
13
  pool: Ecto.Adapters.SQL.Sandbox,
14
  pool_size: System.schedulers_online() * 2
15
16
# We don't run a server during test. If one is required,
17
# you can enable the server option below.
18
config :openagents, OpenAgentsWeb.Endpoint,
19
  http: [ip: {127, 0, 0, 1}, port: 4002],
20
  secret_key_base: "Qg1oZEw1M0GnySDRZ1qJJRZltEFZUQGzBDGsfaLxgKA1EowbYm9EDSYYznEsY8KE",
21
  server: false
22
23
# In test we don't send emails
24
config :openagents, OpenAgents.Mailer, adapter: Swoosh.Adapters.Test
25
26
# Disable swoosh api client as it is only required for production adapters
27
config :swoosh, :api_client, false
28
29
# Print only warnings and errors during test
30
config :logger, level: :warning
31
32
# Initialize plugs at runtime for faster test compilation
33
config :phoenix, :plug_init_mode, :runtime
34
35
# Enable helpful, but potentially expensive runtime checks
36
config :phoenix_live_view,
37
  enable_expensive_runtime_checks: true
38
39
# Sort query params output of verified routes for robust url comparisons
40
config :phoenix,
41
  sort_verified_routes_query_params: true
lib/openagents.ex added +9

@@ -0,0 +1,9 @@

1
defmodule OpenAgents do
2
  @moduledoc """
3
  OpenAgents keeps the contexts that define your domain
4
  and business logic.
5
6
  Contexts are also responsible for managing your data, regardless
7
  if it comes from the database, an external API or others.
8
  """
9
end
lib/openagents/application.ex added +34

@@ -0,0 +1,34 @@

1
defmodule OpenAgents.Application do
2
  # See https://elixir.hexdocs.pm/Application.html
3
  # for more information on OTP Applications
4
  @moduledoc false
5
6
  use Application
7
8
  @impl true
9
  def start(_type, _args) do
10
    children = [
11
      OpenAgentsWeb.Telemetry,
12
      OpenAgents.Repo,
13
      {DNSCluster, query: Application.get_env(:openagents, :dns_cluster_query) || :ignore},
14
      {Phoenix.PubSub, name: OpenAgents.PubSub},
15
      # Start a worker by calling: OpenAgents.Worker.start_link(arg)
16
      # {OpenAgents.Worker, arg},
17
      # Start to serve requests, typically the last entry
18
      OpenAgentsWeb.Endpoint
19
    ]
20
21
    # See https://elixir.hexdocs.pm/Supervisor.html
22
    # for other strategies and supported options
23
    opts = [strategy: :one_for_one, name: OpenAgents.Supervisor]
24
    Supervisor.start_link(children, opts)
25
  end
26
27
  # Tell Phoenix to update the endpoint configuration
28
  # whenever the application is updated.
29
  @impl true
30
  def config_change(changed, _new, removed) do
31
    OpenAgentsWeb.Endpoint.config_change(changed, removed)
32
    :ok
33
  end
34
end
lib/openagents/mailer.ex added +3

@@ -0,0 +1,3 @@

1
defmodule OpenAgents.Mailer do
2
  use Swoosh.Mailer, otp_app: :openagents
3
end
lib/openagents/repo.ex added +5

@@ -0,0 +1,5 @@

1
defmodule OpenAgents.Repo do
2
  use Ecto.Repo,
3
    otp_app: :openagents,
4
    adapter: Ecto.Adapters.Postgres
5
end
lib/openagents_web.ex added +114

@@ -0,0 +1,114 @@

1
defmodule OpenAgentsWeb do
2
  @moduledoc """
3
  The entrypoint for defining your web interface, such
4
  as controllers, components, channels, and so on.
5
6
  This can be used in your application as:
7
8
      use OpenAgentsWeb, :controller
9
      use OpenAgentsWeb, :html
10
11
  The definitions below will be executed for every controller,
12
  component, etc, so keep them short and clean, focused
13
  on imports, uses and aliases.
14
15
  Do NOT define functions inside the quoted expressions
16
  below. Instead, define additional modules and import
17
  those modules here.
18
  """
19
20
  def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
21
22
  def router do
23
    quote do
24
      use Phoenix.Router, helpers: false
25
26
      # Import common connection and controller functions to use in pipelines
27
      import Plug.Conn
28
      import Phoenix.Controller
29
      import Phoenix.LiveView.Router
30
    end
31
  end
32
33
  def channel do
34
    quote do
35
      use Phoenix.Channel
36
    end
37
  end
38
39
  def controller do
40
    quote do
41
      use Phoenix.Controller, formats: [:html, :json]
42
43
      use Gettext, backend: OpenAgentsWeb.Gettext
44
45
      import Plug.Conn
46
47
      unquote(verified_routes())
48
    end
49
  end
50
51
  def live_view do
52
    quote do
53
      use Phoenix.LiveView
54
55
      unquote(html_helpers())
56
    end
57
  end
58
59
  def live_component do
60
    quote do
61
      use Phoenix.LiveComponent
62
63
      unquote(html_helpers())
64
    end
65
  end
66
67
  def html do
68
    quote do
69
      use Phoenix.Component
70
71
      # Import convenience functions from controllers
72
      import Phoenix.Controller,
73
        only: [get_csrf_token: 0, view_module: 1, view_template: 1]
74
75
      # Include general helpers for rendering HTML
76
      unquote(html_helpers())
77
    end
78
  end
79
80
  defp html_helpers do
81
    quote do
82
      # Translation
83
      use Gettext, backend: OpenAgentsWeb.Gettext
84
85
      # HTML escaping functionality
86
      import Phoenix.HTML
87
      # Core UI components
88
      import OpenAgentsWeb.CoreComponents
89
90
      # Common modules used in templates
91
      alias Phoenix.LiveView.JS
92
      alias OpenAgentsWeb.Layouts
93
94
      # Routes generation with the ~p sigil
95
      unquote(verified_routes())
96
    end
97
  end
98
99
  def verified_routes do
100
    quote do
101
      use Phoenix.VerifiedRoutes,
102
        endpoint: OpenAgentsWeb.Endpoint,
103
        router: OpenAgentsWeb.Router,
104
        statics: OpenAgentsWeb.static_paths()
105
    end
106
  end
107
108
  @doc """
109
  When used, dispatch to the appropriate controller/live_view/etc.
110
  """
111
  defmacro __using__(which) when is_atom(which) do
112
    apply(__MODULE__, which, [])
113
  end
114
end
lib/openagents_web/components/core_components.ex added +505

@@ -0,0 +1,505 @@

1
defmodule OpenAgentsWeb.CoreComponents do
2
  @moduledoc """
3
  Provides core UI components.
4
5
  At first glance, this module may seem daunting, but its goal is to provide
6
  core building blocks for your application, such as tables, forms, and
7
  inputs. The components consist mostly of markup and are well-documented
8
  with doc strings and declarative assigns. You may customize and style
9
  them in any way you want, based on your application growth and needs.
10
11
  The foundation for styling is Tailwind CSS, a utility-first CSS framework,
12
  augmented with daisyUI, a Tailwind CSS plugin that provides UI components
13
  and themes. Here are useful references:
14
15
    * [daisyUI](https://daisyui.com/docs/intro/) - a good place to get
16
      started and see the available components.
17
18
    * [Tailwind CSS](https://tailwindcss.com) - the foundational framework
19
      we build on. You will use it for layout, sizing, flexbox, grid, and
20
      spacing.
21
22
    * [Heroicons](https://heroicons.com) - see `icon/1` for usage.
23
24
    * [Phoenix.Component](https://phoenix-live-view.hexdocs.pm/Phoenix.Component.html) -
25
      the component system used by Phoenix. Some components, such as `<.link>`
26
      and `<.form>`, are defined there.
27
28
  """
29
  use Phoenix.Component
30
  use Gettext, backend: OpenAgentsWeb.Gettext
31
32
  alias Phoenix.LiveView.JS
33
34
  @doc """
35
  Renders flash notices.
36
37
  ## Examples
38
39
      <.flash kind={:info} flash={@flash} />
40
      <.flash
41
        id="welcome-back"
42
        kind={:info}
43
        phx-mounted={show("#welcome-back") |> JS.remove_attribute("hidden")}
44
        hidden
45
      >
46
        Welcome Back!
47
      </.flash>
48
  """
49
  attr :id, :string, doc: "the optional id of flash container"
50
  attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
51
  attr :title, :string, default: nil
52
  attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
53
  attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
54
55
  slot :inner_block, doc: "the optional inner block that renders the flash message"
56
57
  def flash(assigns) do
58
    assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
59
60
    ~H"""
61
    <div
62
      :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
63
      id={@id}
64
      phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
65
      role="alert"
66
      class="toast toast-top toast-end z-50"
67
      {@rest}
68
    >
69
      <div class={[
70
        "alert w-80 sm:w-96 max-w-80 sm:max-w-96 text-wrap",
71
        @kind == :info && "alert-info",
72
        @kind == :error && "alert-error"
73
      ]}>
74
        <.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" />
75
        <.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" />
76
        <div>
77
          <p :if={@title} class="font-semibold">{@title}</p>
78
          <p>{msg}</p>
79
        </div>
80
        <div class="flex-1" />
81
        <button type="button" class="group self-start cursor-pointer" aria-label={gettext("close")}>
82
          <.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
83
        </button>
84
      </div>
85
    </div>
86
    """
87
  end
88
89
  @doc """
90
  Renders a button with navigation support.
91
92
  ## Examples
93
94
      <.button>Send!</.button>
95
      <.button phx-click="go" variant="primary">Send!</.button>
96
      <.button navigate={~p"/"}>Home</.button>
97
  """
98
  attr :rest, :global, include: ~w(href navigate patch method download name value disabled)
99
  attr :class, :any
100
  attr :variant, :string, values: ~w(primary)
101
  slot :inner_block, required: true
102
103
  def button(%{rest: rest} = assigns) do
104
    variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
105
106
    assigns =
107
      assign_new(assigns, :class, fn ->
108
        ["btn", Map.fetch!(variants, assigns[:variant])]
109
      end)
110
111
    if rest[:href] || rest[:navigate] || rest[:patch] do
112
      ~H"""
113
      <.link class={@class} {@rest}>
114
        {render_slot(@inner_block)}
115
      </.link>
116
      """
117
    else
118
      ~H"""
119
      <button class={@class} {@rest}>
120
        {render_slot(@inner_block)}
121
      </button>
122
      """
123
    end
124
  end
125
126
  @doc """
127
  Renders an input with label and error messages.
128
129
  A `Phoenix.HTML.FormField` may be passed as argument,
130
  which is used to retrieve the input name, id, and values.
131
  Otherwise all attributes may be passed explicitly.
132
133
  ## Types
134
135
  This function accepts all HTML input types, considering that:
136
137
    * You may also set `type="select"` to render a `<select>` tag
138
139
    * `type="checkbox"` is used exclusively to render boolean values
140
141
    * For live file uploads, see `Phoenix.Component.live_file_input/1`
142
143
  See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
144
  for more information. Unsupported types, such as radio, are best
145
  written directly in your templates.
146
147
  ## Examples
148
149
  ```heex
150
  <.input field={@form[:email]} type="email" />
151
  <.input name="my-input" errors={["oh no!"]} />
152
  ```
153
154
  ## Select type
155
156
  When using `type="select"`, you must pass the `options` and optionally
157
  a `value` to mark which option should be preselected.
158
159
  ```heex
160
  <.input field={@form[:user_type]} type="select" options={["Admin": "admin", "User": "user"]} />
161
  ```
162
163
  For more information on what kind of data can be passed to `options` see
164
  [`options_for_select`](https://phoenix-html.hexdocs.pm/Phoenix.HTML.Form.html#options_for_select/2).
165
  """
166
  attr :id, :any, default: nil
167
  attr :name, :any
168
  attr :label, :string, default: nil
169
  attr :value, :any
170
171
  attr :type, :string,
172
    default: "text",
173
    values: ~w(checkbox color date datetime-local email file month number password
174
               search select tel text textarea time url week hidden)
175
176
  attr :field, Phoenix.HTML.FormField,
177
    doc: "a form field struct retrieved from the form, for example: @form[:email]"
178
179
  attr :errors, :list, default: []
180
  attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
181
  attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
182
  attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
183
  attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
184
  attr :class, :any, default: nil, doc: "the input class to use over defaults"
185
  attr :error_class, :any, default: nil, doc: "the input error class to use over defaults"
186
187
  attr :rest, :global,
188
    include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
189
                multiple pattern placeholder readonly required rows size step)
190
191
  def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
192
    errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
193
194
    assigns
195
    |> assign(field: nil, id: assigns.id || field.id)
196
    |> assign(:errors, Enum.map(errors, &translate_error(&1)))
197
    |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
198
    |> assign_new(:value, fn -> field.value end)
199
    |> input()
200
  end
201
202
  def input(%{type: "hidden"} = assigns) do
203
    ~H"""
204
    <input type="hidden" id={@id} name={@name} value={@value} {@rest} />
205
    """
206
  end
207
208
  def input(%{type: "checkbox"} = assigns) do
209
    assigns =
210
      assign_new(assigns, :checked, fn ->
211
        Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
212
      end)
213
214
    ~H"""
215
    <div class="fieldset mb-2">
216
      <label for={@id}>
217
        <input
218
          type="hidden"
219
          name={@name}
220
          value="false"
221
          disabled={@rest[:disabled]}
222
          form={@rest[:form]}
223
        />
224
        <span class="label">
225
          <input
226
            type="checkbox"
227
            id={@id}
228
            name={@name}
229
            value="true"
230
            checked={@checked}
231
            class={@class || "checkbox checkbox-sm"}
232
            {@rest}
233
          />{@label}
234
        </span>
235
      </label>
236
      <.error :for={msg <- @errors}>{msg}</.error>
237
    </div>
238
    """
239
  end
240
241
  def input(%{type: "select"} = assigns) do
242
    ~H"""
243
    <div class="fieldset mb-2">
244
      <label for={@id}>
245
        <span :if={@label} class="label mb-1">{@label}</span>
246
        <select
247
          id={@id}
248
          name={@name}
249
          class={[@class || "w-full select", @errors != [] && (@error_class || "select-error")]}
250
          multiple={@multiple}
251
          {@rest}
252
        >
253
          <option :if={@prompt} value="">{@prompt}</option>
254
          {Phoenix.HTML.Form.options_for_select(@options, @value)}
255
        </select>
256
      </label>
257
      <.error :for={msg <- @errors}>{msg}</.error>
258
    </div>
259
    """
260
  end
261
262
  def input(%{type: "textarea"} = assigns) do
263
    ~H"""
264
    <div class="fieldset mb-2">
265
      <label for={@id}>
266
        <span :if={@label} class="label mb-1">{@label}</span>
267
        <textarea
268
          id={@id}
269
          name={@name}
270
          class={[
271
            @class || "w-full textarea",
272
            @errors != [] && (@error_class || "textarea-error")
273
          ]}
274
          {@rest}
275
        >{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
276
      </label>
277
      <.error :for={msg <- @errors}>{msg}</.error>
278
    </div>
279
    """
280
  end
281
282
  # All other inputs text, datetime-local, url, password, etc. are handled here...
283
  def input(assigns) do
284
    ~H"""
285
    <div class="fieldset mb-2">
286
      <label for={@id}>
287
        <span :if={@label} class="label mb-1">{@label}</span>
288
        <input
289
          type={@type}
290
          name={@name}
291
          id={@id}
292
          value={Phoenix.HTML.Form.normalize_value(@type, @value)}
293
          class={[
294
            @class || "w-full input",
295
            @errors != [] && (@error_class || "input-error")
296
          ]}
297
          {@rest}
298
        />
299
      </label>
300
      <.error :for={msg <- @errors}>{msg}</.error>
301
    </div>
302
    """
303
  end
304
305
  # Helper used by inputs to generate form errors
306
  defp error(assigns) do
307
    ~H"""
308
    <p class="mt-1.5 flex gap-2 items-center text-sm text-error">
309
      <.icon name="hero-exclamation-circle" class="size-5" />
310
      {render_slot(@inner_block)}
311
    </p>
312
    """
313
  end
314
315
  @doc """
316
  Renders a header with title.
317
  """
318
  slot :inner_block, required: true
319
  slot :subtitle
320
  slot :actions
321
322
  def header(assigns) do
323
    ~H"""
324
    <header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4"]}>
325
      <div>
326
        <h1 class="text-lg font-semibold leading-8">
327
          {render_slot(@inner_block)}
328
        </h1>
329
        <p :if={@subtitle != []} class="text-sm text-base-content/70">
330
          {render_slot(@subtitle)}
331
        </p>
332
      </div>
333
      <div class="flex-none">{render_slot(@actions)}</div>
334
    </header>
335
    """
336
  end
337
338
  @doc """
339
  Renders a table with generic styling.
340
341
  ## Examples
342
343
      <.table id="users" rows={@users}>
344
        <:col :let={user} label="id">{user.id}</:col>
345
        <:col :let={user} label="username">{user.username}</:col>
346
      </.table>
347
  """
348
  attr :id, :string, required: true
349
  attr :rows, :list, required: true
350
  attr :row_id, :any, default: nil, doc: "the function for generating the row id"
351
  attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
352
353
  attr :row_item, :any,
354
    default: &Function.identity/1,
355
    doc: "the function for mapping each row before calling the :col and :action slots"
356
357
  slot :col, required: true do
358
    attr :label, :string
359
  end
360
361
  slot :action, doc: "the slot for showing user actions in the last table column"
362
363
  def table(assigns) do
364
    assigns =
365
      with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
366
        assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
367
      end
368
369
    ~H"""
370
    <table class="table table-zebra">
371
      <thead>
372
        <tr>
373
          <th :for={col <- @col}>{col[:label]}</th>
374
          <th :if={@action != []}>
375
            <span class="sr-only">{gettext("Actions")}</span>
376
          </th>
377
        </tr>
378
      </thead>
379
      <tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
380
        <tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
381
          <td
382
            :for={col <- @col}
383
            phx-click={@row_click && @row_click.(row)}
384
            class={@row_click && "hover:cursor-pointer"}
385
          >
386
            {render_slot(col, @row_item.(row))}
387
          </td>
388
          <td :if={@action != []} class="w-0 font-semibold">
389
            <div class="flex gap-4">
390
              <%= for action <- @action do %>
391
                {render_slot(action, @row_item.(row))}
392
              <% end %>
393
            </div>
394
          </td>
395
        </tr>
396
      </tbody>
397
    </table>
398
    """
399
  end
400
401
  @doc """
402
  Renders a data list.
403
404
  ## Examples
405
406
      <.list>
407
        <:item title="Title">{@post.title}</:item>
408
        <:item title="Views">{@post.views}</:item>
409
      </.list>
410
  """
411
  slot :item, required: true do
412
    attr :title, :string, required: true
413
  end
414
415
  def list(assigns) do
416
    ~H"""
417
    <ul class="list">
418
      <li :for={item <- @item} class="list-row">
419
        <div class="list-col-grow">
420
          <div class="font-bold">{item.title}</div>
421
          <div>{render_slot(item)}</div>
422
        </div>
423
      </li>
424
    </ul>
425
    """
426
  end
427
428
  @doc """
429
  Renders a [Heroicon](https://heroicons.com).
430
431
  Heroicons come in three styles – outline, solid, and mini.
432
  By default, the outline style is used, but solid and mini may
433
  be applied by using the `-solid` and `-mini` suffix.
434
435
  You can customize the size and colors of the icons by setting
436
  width, height, and background color classes.
437
438
  Icons are extracted from the `deps/heroicons` directory and bundled within
439
  your compiled app.css by the plugin in `assets/vendor/heroicons.js`.
440
441
  ## Examples
442
443
      <.icon name="hero-x-mark" />
444
      <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
445
  """
446
  attr :name, :string, required: true
447
  attr :class, :any, default: "size-4"
448
449
  def icon(%{name: "hero-" <> _} = assigns) do
450
    ~H"""
451
    <span class={[@name, @class]} />
452
    """
453
  end
454
455
  ## JS Commands
456
457
  def show(js \\ %JS{}, selector) do
458
    JS.show(js,
459
      to: selector,
460
      time: 300,
461
      transition:
462
        {"transition-all ease-out duration-300",
463
         "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
464
         "opacity-100 translate-y-0 sm:scale-100"}
465
    )
466
  end
467
468
  def hide(js \\ %JS{}, selector) do
469
    JS.hide(js,
470
      to: selector,
471
      time: 200,
472
      transition:
473
        {"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100",
474
         "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
475
    )
476
  end
477
478
  @doc """
479
  Translates an error message using gettext.
480
  """
481
  def translate_error({msg, opts}) do
482
    # When using gettext, we typically pass the strings we want
483
    # to translate as a static argument:
484
    #
485
    #     # Translate the number of files with plural rules
486
    #     dngettext("errors", "1 file", "%{count} files", count)
487
    #
488
    # However the error messages in our forms and APIs are generated
489
    # dynamically, so we need to translate them by calling Gettext
490
    # with our gettext backend as first argument. Translations are
491
    # available in the errors.po file (as we use the "errors" domain).
492
    if count = opts[:count] do
493
      Gettext.dngettext(OpenAgentsWeb.Gettext, "errors", msg, msg, count, opts)
494
    else
495
      Gettext.dgettext(OpenAgentsWeb.Gettext, "errors", msg, opts)
496
    end
497
  end
498
499
  @doc """
500
  Translates the errors for a field from a keyword list of errors.
501
  """
502
  def translate_errors(errors, field) when is_list(errors) do
503
    for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
504
  end
505
end
lib/openagents_web/components/layouts.ex added +160

@@ -0,0 +1,160 @@

1
defmodule OpenAgentsWeb.Layouts do
2
  @moduledoc """
3
  This module holds layouts and related functionality
4
  used by your application.
5
  """
6
  use OpenAgentsWeb, :html
7
8
  # Embed all files in layouts/* within this module.
9
  # The default root.html.heex file contains the HTML
10
  # skeleton of your application, namely HTML headers
11
  # and other static content.
12
  embed_templates "layouts/*"
13
14
  @doc """
15
  Renders your app layout.
16
17
  This function is typically invoked from every template,
18
  and it often contains your application menu, sidebar,
19
  or similar.
20
21
  ## Examples
22
23
      <Layouts.app flash={@flash}>
24
        <h1>Content</h1>
25
      </Layouts.app>
26
27
  """
28
  attr :flash, :map, required: true, doc: "the map of flash messages"
29
30
  attr :current_scope, :map,
31
    default: nil,
32
    doc: "the current [scope](https://phoenix.hexdocs.pm/scopes.html)"
33
34
  slot :inner_block, required: true
35
36
  def app(assigns) do
37
    ~H"""
38
    <header class="navbar px-4 sm:px-6 lg:px-8">
39
      <div class="flex-1">
40
        <a href="/" class="flex-1 flex w-fit items-center gap-2">
41
          <img src={~p"/images/logo.svg"} width="36" />
42
          <span class="text-sm font-semibold">v{Application.spec(:phoenix, :vsn)}</span>
43
        </a>
44
      </div>
45
      <div class="flex-none">
46
        <ul class="flex flex-column px-1 space-x-4 items-center">
47
          <li>
48
            <a href="https://phoenixframework.org/" class="btn btn-ghost">Website</a>
49
          </li>
50
          <li>
51
            <a href="https://github.com/phoenixframework/phoenix" class="btn btn-ghost">GitHub</a>
52
          </li>
53
          <li>
54
            <.theme_toggle />
55
          </li>
56
          <li>
57
            <a href="https://phoenix.hexdocs.pm/overview.html" class="btn btn-primary">
58
              Get Started <span aria-hidden="true">&rarr;</span>
59
            </a>
60
          </li>
61
        </ul>
62
      </div>
63
    </header>
64
65
    <main class="px-4 py-20 sm:px-6 lg:px-8">
66
      <div class="mx-auto max-w-2xl space-y-4">
67
        {render_slot(@inner_block)}
68
      </div>
69
    </main>
70
71
    <.flash_group flash={@flash} />
72
    """
73
  end
74
75
  @doc """
76
  Shows the flash group with standard titles and content.
77
78
  ## Examples
79
80
      <.flash_group flash={@flash} />
81
  """
82
  attr :flash, :map, required: true, doc: "the map of flash messages"
83
  attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
84
85
  def flash_group(assigns) do
86
    ~H"""
87
    <div id={@id} aria-live="polite">
88
      <.flash kind={:info} flash={@flash} />
89
      <.flash kind={:error} flash={@flash} />
90
91
      <.flash
92
        id="client-error"
93
        kind={:error}
94
        title={gettext("We can't find the internet")}
95
        phx-disconnected={
96
          show(".phx-client-error #client-error")
97
          |> JS.remove_attribute("hidden", to: ".phx-client-error #client-error")
98
        }
99
        phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
100
        hidden
101
      >
102
        {gettext("Attempting to reconnect")}
103
        <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
104
      </.flash>
105
106
      <.flash
107
        id="server-error"
108
        kind={:error}
109
        title={gettext("Something went wrong!")}
110
        phx-disconnected={
111
          show(".phx-server-error #server-error")
112
          |> JS.remove_attribute("hidden", to: ".phx-server-error #server-error")
113
        }
114
        phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})}
115
        hidden
116
      >
117
        {gettext("Attempting to reconnect")}
118
        <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
119
      </.flash>
120
    </div>
121
    """
122
  end
123
124
  @doc """
125
  Provides dark vs light theme toggle based on themes defined in app.css.
126
127
  See <head> in root.html.heex which applies the theme before page load.
128
  """
129
  def theme_toggle(assigns) do
130
    ~H"""
131
    <div class="card relative flex flex-row items-center border-2 border-base-300 bg-base-300 rounded-full">
132
      <div class="absolute w-1/3 h-full rounded-full border-1 border-base-200 bg-base-100 brightness-200 left-0 [[data-theme=light]_&]:left-1/3 [[data-theme=dark]_&]:left-2/3 [[data-theme-source=system]_&]:!left-0 transition-[left]" />
133
134
      <button
135
        class="flex p-2 cursor-pointer w-1/3"
136
        phx-click={JS.dispatch("phx:set-theme")}
137
        data-phx-theme="system"
138
      >
139
        <.icon name="hero-computer-desktop-micro" class="size-4 opacity-75 hover:opacity-100" />
140
      </button>
141
142
      <button
143
        class="flex p-2 cursor-pointer w-1/3"
144
        phx-click={JS.dispatch("phx:set-theme")}
145
        data-phx-theme="light"
146
      >
147
        <.icon name="hero-sun-micro" class="size-4 opacity-75 hover:opacity-100" />
148
      </button>
149
150
      <button
151
        class="flex p-2 cursor-pointer w-1/3"
152
        phx-click={JS.dispatch("phx:set-theme")}
153
        data-phx-theme="dark"
154
      >
155
        <.icon name="hero-moon-micro" class="size-4 opacity-75 hover:opacity-100" />
156
      </button>
157
    </div>
158
    """
159
  end
160
end
lib/openagents_web/components/layouts/root.html.heex added +43

@@ -0,0 +1,43 @@

1
<!DOCTYPE html>
2
<html lang="en">
3
  <head>
4
    <meta charset="utf-8" />
5
    <meta name="viewport" content="width=device-width, initial-scale=1" />
6
    <meta name="csrf-token" content={get_csrf_token()} />
7
    <.live_title default="OpenAgents" suffix=" · Phoenix Framework" phx-no-format>{assigns[:page_title]}</.live_title>
8
    <link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
9
    <script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}>
10
    </script>
11
    <script>
12
      (() => {
13
        const systemTheme = () => matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
14
15
        const setTheme = (theme) => {
16
          if (theme === "system") {
17
            localStorage.removeItem("phx:theme");
18
            document.documentElement.setAttribute("data-theme", systemTheme());
19
            document.documentElement.setAttribute("data-theme-source", "system");
20
          } else {
21
            localStorage.setItem("phx:theme", theme);
22
            document.documentElement.setAttribute("data-theme", theme);
23
            document.documentElement.setAttribute("data-theme-source", "user");
24
          }
25
        };
26
        if (!document.documentElement.hasAttribute("data-theme")) {
27
          setTheme(localStorage.getItem("phx:theme") || "system");
28
        }
29
        window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
30
        window.addEventListener("phx:set-theme", (e) => setTheme(e.target.dataset.phxTheme));
31
32
        matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
33
          if (document.documentElement.getAttribute("data-theme-source") === "system") {
34
            document.documentElement.setAttribute("data-theme", systemTheme());
35
          }
36
        });
37
      })();
38
    </script>
39
  </head>
40
  <body>
41
    {@inner_content}
42
  </body>
43
</html>
lib/openagents_web/controllers/error_html.ex added +24

@@ -0,0 +1,24 @@

1
defmodule OpenAgentsWeb.ErrorHTML do
2
  @moduledoc """
3
  This module is invoked by your endpoint in case of errors on HTML requests.
4
5
  See config/config.exs.
6
  """
7
  use OpenAgentsWeb, :html
8
9
  # If you want to customize your error pages,
10
  # uncomment the embed_templates/1 call below
11
  # and add pages to the error directory:
12
  #
13
  #   * lib/openagents_web/controllers/error_html/404.html.heex
14
  #   * lib/openagents_web/controllers/error_html/500.html.heex
15
  #
16
  # embed_templates "error_html/*"
17
18
  # The default is to render a plain text page based on
19
  # the template name. For example, "404.html" becomes
20
  # "Not Found".
21
  def render(template, _assigns) do
22
    Phoenix.Controller.status_message_from_template(template)
23
  end
24
end
lib/openagents_web/controllers/error_json.ex added +21

@@ -0,0 +1,21 @@

1
defmodule OpenAgentsWeb.ErrorJSON do
2
  @moduledoc """
3
  This module is invoked by your endpoint in case of errors on JSON requests.
4
5
  See config/config.exs.
6
  """
7
8
  # If you want to customize a particular status code,
9
  # you may add your own clauses, such as:
10
  #
11
  # def render("500.json", _assigns) do
12
  #   %{errors: %{detail: "Internal Server Error"}}
13
  # end
14
15
  # By default, Phoenix returns the status message from
16
  # the template name. For example, "404.json" becomes
17
  # "Not Found".
18
  def render(template, _assigns) do
19
    %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
20
  end
21
end
lib/openagents_web/controllers/page_controller.ex added +7

@@ -0,0 +1,7 @@

1
defmodule OpenAgentsWeb.PageController do
2
  use OpenAgentsWeb, :controller
3
4
  def home(conn, _params) do
5
    render(conn, :home)
6
  end
7
end
lib/openagents_web/controllers/page_html.ex added +10

@@ -0,0 +1,10 @@

1
defmodule OpenAgentsWeb.PageHTML do
2
  @moduledoc """
3
  This module contains pages rendered by PageController.
4
5
  See the `page_html` directory for all templates available.
6
  """
7
  use OpenAgentsWeb, :html
8
9
  embed_templates "page_html/*"
10
end
lib/openagents_web/controllers/page_html/home.html.heex added +199

@@ -0,0 +1,199 @@

1
<Layouts.flash_group flash={@flash} />
2
<div class="left-[40rem] fixed inset-y-0 right-0 z-0 hidden lg:block xl:left-[50rem]">
3
  <svg
4
    viewBox="0 0 1480 957"
5
    fill="none"
6
    aria-hidden="true"
7
    class="absolute inset-0 h-full w-full"
8
    preserveAspectRatio="xMinYMid slice"
9
  >
10
    <path fill="#EE7868" d="M0 0h1480v957H0z" />
11
    <path
12
      d="M137.542 466.27c-582.851-48.41-988.806-82.127-1608.412 658.2l67.39 810 3083.15-256.51L1535.94-49.622l-98.36 8.183C1269.29 281.468 734.115 515.799 146.47 467.012l-8.928-.742Z"
13
      fill="#FF9F92"
14
    />
15
    <path
16
      d="M371.028 528.664C-169.369 304.988-545.754 149.198-1361.45 665.565l-182.58 792.025 3014.73 694.98 389.42-1689.25-96.18-22.171C1505.28 697.438 924.153 757.586 379.305 532.09l-8.277-3.426Z"
17
      fill="#FA8372"
18
    />
19
    <path
20
      d="M359.326 571.714C-104.765 215.795-428.003-32.102-1349.55 255.554l-282.3 1224.596 3047.04 722.01 312.24-1354.467C1411.25 1028.3 834.355 935.995 366.435 577.166l-7.109-5.452Z"
21
      fill="#E96856"
22
      fill-opacity=".6"
23
    />
24
    <path
25
      d="M1593.87 1236.88c-352.15 92.63-885.498-145.85-1244.602-613.557l-5.455-7.105C-12.347 152.31-260.41-170.8-1225-131.458l-368.63 1599.048 3057.19 704.76 130.31-935.47Z"
26
      fill="#C42652"
27
      fill-opacity=".2"
28
    />
29
    <path
30
      d="M1411.91 1526.93c-363.79 15.71-834.312-330.6-1085.883-863.909l-3.822-8.102C72.704 125.95-101.074-242.476-1052.01-408.907l-699.85 1484.267 2837.75 1338.01 326.02-886.44Z"
31
      fill="#A41C42"
32
      fill-opacity=".2"
33
    />
34
    <path
35
      d="M1116.26 1863.69c-355.457-78.98-720.318-535.27-825.287-1115.521l-1.594-8.816C185.286 163.833 112.786-237.016-762.678-643.898L-1822.83 608.665 571.922 2635.55l544.338-771.86Z"
36
      fill="#A41C42"
37
      fill-opacity=".2"
38
    />
39
  </svg>
40
</div>
41
<div class="px-4 py-10 sm:px-6 sm:py-28 lg:px-8 xl:px-28 xl:py-32">
42
  <div class="mx-auto max-w-xl lg:mx-0">
43
    <svg viewBox="0 0 71 48" class="h-12" aria-hidden="true">
44
      <path
45
        d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
46
        fill="#FD4F00"
47
      />
48
    </svg>
49
    <div class="mt-10 flex justify-between items-center">
50
      <h1 class="flex items-center text-sm font-semibold leading-6">
51
        Phoenix Framework
52
        <small class="badge badge-warning badge-sm ml-3">
53
          v{Application.spec(:phoenix, :vsn)}
54
        </small>
55
      </h1>
56
      <Layouts.theme_toggle />
57
    </div>
58
59
    <p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-balance">
60
      Peace of mind from prototype to production.
61
    </p>
62
    <p class="mt-4 leading-7 text-base-content/70">
63
      Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
64
    </p>
65
    <div class="flex">
66
      <div class="w-full sm:w-auto">
67
        <div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
68
          <a
69
            href="https://phoenix.hexdocs.pm/overview.html"
70
            class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
71
          >
72
            <span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105"></span>
73
            <span class="relative flex items-center gap-4 sm:flex-col">
74
              <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
75
                <path d="m12 4 10-2v18l-10 2V4Z" fill="currentColor" fill-opacity=".15" />
76
                <path
77
                  d="M12 4 2 2v18l10 2m0-18v18m0-18 10-2v18l-10 2"
78
                  stroke="currentColor"
79
                  stroke-width="2"
80
                  stroke-linecap="round"
81
                  stroke-linejoin="round"
82
                />
83
              </svg>
84
              Guides &amp; Docs
85
            </span>
86
          </a>
87
          <a
88
            href="https://github.com/phoenixframework/phoenix"
89
            class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
90
          >
91
            <span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105"></span>
92
            <span class="relative flex items-center gap-4 sm:flex-col">
93
              <svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
94
                <path
95
                  fill="currentColor"
96
                  fill-rule="evenodd"
97
                  clip-rule="evenodd"
98
                  d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489A12.536 12.536 0 0 0 24 12.304C24 5.505 18.63 0 12 0Z"
99
                />
100
              </svg>
101
              Source Code
102
            </span>
103
          </a>
104
          <a
105
            href={"https://github.com/phoenixframework/phoenix/blob/v#{Application.spec(:phoenix, :vsn)}/CHANGELOG.md"}
106
            class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
107
          >
108
            <span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105"></span>
109
            <span class="relative flex items-center gap-4 sm:flex-col">
110
              <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
111
                <path
112
                  d="M12 1v6M12 17v6"
113
                  stroke="currentColor"
114
                  stroke-width="2"
115
                  stroke-linecap="round"
116
                  stroke-linejoin="round"
117
                />
118
                <circle
119
                  cx="12"
120
                  cy="12"
121
                  r="4"
122
                  fill="currentColor"
123
                  fill-opacity=".15"
124
                  stroke="currentColor"
125
                  stroke-width="2"
126
                  stroke-linecap="round"
127
                  stroke-linejoin="round"
128
                />
129
              </svg>
130
              Changelog
131
            </span>
132
          </a>
133
        </div>
134
        <div class="mt-10 grid grid-cols-1 gap-y-4 text-sm leading-6 text-base-content/80 sm:grid-cols-2">
135
          <div>
136
            <a
137
              href="https://elixirforum.com"
138
              class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
139
            >
140
              <svg
141
                viewBox="0 0 16 16"
142
                aria-hidden="true"
143
                class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
144
              >
145
                <path d="M8 13.833c3.866 0 7-2.873 7-6.416C15 3.873 11.866 1 8 1S1 3.873 1 7.417c0 1.081.292 2.1.808 2.995.606 1.05.806 2.399.086 3.375l-.208.283c-.285.386-.01.905.465.85.852-.098 2.048-.318 3.137-.81a3.717 3.717 0 0 1 1.91-.318c.263.027.53.041.802.041Z" />
146
              </svg>
147
              Discuss on the Elixir Forum
148
            </a>
149
          </div>
150
          <div>
151
            <a
152
              href="https://discord.gg/elixir"
153
              class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
154
            >
155
              <svg
156
                viewBox="0 0 16 16"
157
                aria-hidden="true"
158
                class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
159
              >
160
                <path d="M13.545 2.995c-1.02-.46-2.114-.8-3.257-.994a.05.05 0 0 0-.052.024c-.141.246-.297.567-.406.82a12.377 12.377 0 0 0-3.658 0 8.238 8.238 0 0 0-.412-.82.052.052 0 0 0-.052-.024 13.315 13.315 0 0 0-3.257.994.046.046 0 0 0-.021.018C.356 6.063-.213 9.036.066 11.973c.001.015.01.029.02.038a13.353 13.353 0 0 0 3.996 1.987.052.052 0 0 0 .056-.018c.308-.414.582-.85.818-1.309a.05.05 0 0 0-.028-.069 8.808 8.808 0 0 1-1.248-.585.05.05 0 0 1-.005-.084c.084-.062.168-.126.248-.191a.05.05 0 0 1 .051-.007c2.619 1.176 5.454 1.176 8.041 0a.05.05 0 0 1 .053.006c.08.065.164.13.248.192a.05.05 0 0 1-.004.084c-.399.23-.813.423-1.249.585a.05.05 0 0 0-.027.07c.24.457.514.893.817 1.307a.051.051 0 0 0 .056.019 13.31 13.31 0 0 0 4.001-1.987.05.05 0 0 0 .021-.037c.334-3.396-.559-6.345-2.365-8.96a.04.04 0 0 0-.021-.02Zm-8.198 7.19c-.789 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.637 1.587-1.438 1.587Zm5.316 0c-.788 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.63 1.587-1.438 1.587Z" />
161
              </svg>
162
              Join our Discord server
163
            </a>
164
          </div>
165
          <div>
166
            <a
167
              href="https://elixir-slack.community/"
168
              class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
169
            >
170
              <svg
171
                viewBox="0 0 16 16"
172
                aria-hidden="true"
173
                class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
174
              >
175
                <path d="M3.361 10.11a1.68 1.68 0 1 1-1.68-1.681h1.68v1.682ZM4.209 10.11a1.68 1.68 0 1 1 3.361 0v4.21a1.68 1.68 0 1 1-3.361 0v-4.21ZM5.89 3.361a1.68 1.68 0 1 1 1.681-1.68v1.68H5.89ZM5.89 4.209a1.68 1.68 0 1 1 0 3.361H1.68a1.68 1.68 0 1 1 0-3.361h4.21ZM12.639 5.89a1.68 1.68 0 1 1 1.68 1.681h-1.68V5.89ZM11.791 5.89a1.68 1.68 0 1 1-3.361 0V1.68a1.68 1.68 0 0 1 3.361 0v4.21ZM10.11 12.639a1.68 1.68 0 1 1-1.681 1.68v-1.68h1.682ZM10.11 11.791a1.68 1.68 0 1 1 0-3.361h4.21a1.68 1.68 0 1 1 0 3.361h-4.21Z" />
176
              </svg>
177
              Join us on Slack
178
            </a>
179
          </div>
180
          <div>
181
            <a
182
              href="https://fly.io/docs/elixir/getting-started/"
183
              class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
184
            >
185
              <svg
186
                viewBox="0 0 20 20"
187
                aria-hidden="true"
188
                class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
189
              >
190
                <path d="M1 12.5A4.5 4.5 0 005.5 17H15a4 4 0 001.866-7.539 3.504 3.504 0 00-4.504-4.272A4.5 4.5 0 004.06 8.235 4.502 4.502 0 001 12.5z" />
191
              </svg>
192
              Deploy your application
193
            </a>
194
          </div>
195
        </div>
196
      </div>
197
    </div>
198
  </div>
199
</div>
lib/openagents_web/endpoint.ex added +55

@@ -0,0 +1,55 @@

1
defmodule OpenAgentsWeb.Endpoint do
2
  use Phoenix.Endpoint, otp_app: :openagents
3
4
  # The session will be stored in the cookie and signed,
5
  # this means its contents can be read but not tampered with.
6
  # Set :encryption_salt if you would also like to encrypt it.
7
  @session_options [
8
    store: :cookie,
9
    key: "_openagents_key",
10
    signing_salt: "fq1woUNS",
11
    same_site: "Lax"
12
  ]
13
14
  socket "/live", Phoenix.LiveView.Socket,
15
    websocket: [connect_info: [session: @session_options]],
16
    longpoll: [connect_info: [session: @session_options]]
17
18
  # Serve at "/" the static files from "priv/static" directory.
19
  #
20
  # When code reloading is disabled (e.g., in production),
21
  # the `gzip` option is enabled to serve compressed
22
  # static files generated by running `phx.digest`.
23
  plug Plug.Static,
24
    at: "/",
25
    from: :openagents,
26
    gzip: not code_reloading?,
27
    only: OpenAgentsWeb.static_paths(),
28
    raise_on_missing_only: code_reloading?
29
30
  # Code reloading can be explicitly enabled under the
31
  # :code_reloader configuration of your endpoint.
32
  if code_reloading? do
33
    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
34
    plug Phoenix.LiveReloader
35
    plug Phoenix.CodeReloader
36
    plug Phoenix.Ecto.CheckRepoStatus, otp_app: :openagents
37
  end
38
39
  plug Phoenix.LiveDashboard.RequestLogger,
40
    param_key: "request_logger",
41
    cookie_key: "request_logger"
42
43
  plug Plug.RequestId
44
  plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
45
46
  plug Plug.Parsers,
47
    parsers: [:urlencoded, :multipart, :json],
48
    pass: ["*/*"],
49
    json_decoder: Phoenix.json_library()
50
51
  plug Plug.MethodOverride
52
  plug Plug.Head
53
  plug Plug.Session, @session_options
54
  plug OpenAgentsWeb.Router
55
end
lib/openagents_web/gettext.ex added +25

@@ -0,0 +1,25 @@

1
defmodule OpenAgentsWeb.Gettext do
2
  @moduledoc """
3
  A module providing Internationalization with a gettext-based API.
4
5
  By using [Gettext](https://gettext.hexdocs.pm), your module compiles translations
6
  that you can use in your application. To use this Gettext backend module,
7
  call `use Gettext` and pass it as an option:
8
9
      use Gettext, backend: OpenAgentsWeb.Gettext
10
11
      # Simple translation
12
      gettext("Here is the string to translate")
13
14
      # Plural translation
15
      ngettext("Here is the string to translate",
16
               "Here are the strings to translate",
17
               3)
18
19
      # Domain-based translation
20
      dgettext("errors", "Here is the error message to translate")
21
22
  See the [Gettext Docs](https://gettext.hexdocs.pm) for detailed usage.
23
  """
24
  use Gettext.Backend, otp_app: :openagents
25
end
lib/openagents_web/router.ex added +44

@@ -0,0 +1,44 @@

1
defmodule OpenAgentsWeb.Router do
2
  use OpenAgentsWeb, :router
3
4
  pipeline :browser do
5
    plug :accepts, ["html"]
6
    plug :fetch_session
7
    plug :fetch_live_flash
8
    plug :put_root_layout, html: {OpenAgentsWeb.Layouts, :root}
9
    plug :protect_from_forgery
10
    plug :put_secure_browser_headers
11
  end
12
13
  pipeline :api do
14
    plug :accepts, ["json"]
15
  end
16
17
  scope "/", OpenAgentsWeb do
18
    pipe_through :browser
19
20
    get "/", PageController, :home
21
  end
22
23
  # Other scopes may use custom stacks.
24
  # scope "/api", OpenAgentsWeb do
25
  #   pipe_through :api
26
  # end
27
28
  # Enable LiveDashboard and Swoosh mailbox preview in development
29
  if Application.compile_env(:openagents, :dev_routes) do
30
    # If you want to use the LiveDashboard in production, you should put
31
    # it behind authentication and allow only admins to access it.
32
    # If your application does not have an admins-only section yet,
33
    # you can use Plug.BasicAuth to set up some basic authentication
34
    # as long as you are also using SSL (which you should anyway).
35
    import Phoenix.LiveDashboard.Router
36
37
    scope "/dev" do
38
      pipe_through :browser
39
40
      live_dashboard "/dashboard", metrics: OpenAgentsWeb.Telemetry
41
      forward "/mailbox", Plug.Swoosh.MailboxPreview
42
    end
43
  end
44
end
lib/openagents_web/telemetry.ex added +93

@@ -0,0 +1,93 @@

1
defmodule OpenAgentsWeb.Telemetry do
2
  use Supervisor
3
  import Telemetry.Metrics
4
5
  def start_link(arg) do
6
    Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
7
  end
8
9
  @impl true
10
  def init(_arg) do
11
    children = [
12
      # Telemetry poller will execute the given period measurements
13
      # every 10_000ms. Learn more here: https://telemetry-metrics.hexdocs.pm
14
      {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
15
      # Add reporters as children of your supervision tree.
16
      # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
17
    ]
18
19
    Supervisor.init(children, strategy: :one_for_one)
20
  end
21
22
  def metrics do
23
    [
24
      # Phoenix Metrics
25
      summary("phoenix.endpoint.start.system_time",
26
        unit: {:native, :millisecond}
27
      ),
28
      summary("phoenix.endpoint.stop.duration",
29
        unit: {:native, :millisecond}
30
      ),
31
      summary("phoenix.router_dispatch.start.system_time",
32
        tags: [:route],
33
        unit: {:native, :millisecond}
34
      ),
35
      summary("phoenix.router_dispatch.exception.duration",
36
        tags: [:route],
37
        unit: {:native, :millisecond}
38
      ),
39
      summary("phoenix.router_dispatch.stop.duration",
40
        tags: [:route],
41
        unit: {:native, :millisecond}
42
      ),
43
      summary("phoenix.socket_connected.duration",
44
        unit: {:native, :millisecond}
45
      ),
46
      sum("phoenix.socket_drain.count"),
47
      summary("phoenix.channel_joined.duration",
48
        unit: {:native, :millisecond}
49
      ),
50
      summary("phoenix.channel_handled_in.duration",
51
        tags: [:event],
52
        unit: {:native, :millisecond}
53
      ),
54
55
      # Database Metrics
56
      summary("openagents.repo.query.total_time",
57
        unit: {:native, :millisecond},
58
        description: "The sum of the other measurements"
59
      ),
60
      summary("openagents.repo.query.decode_time",
61
        unit: {:native, :millisecond},
62
        description: "The time spent decoding the data received from the database"
63
      ),
64
      summary("openagents.repo.query.query_time",
65
        unit: {:native, :millisecond},
66
        description: "The time spent executing the query"
67
      ),
68
      summary("openagents.repo.query.queue_time",
69
        unit: {:native, :millisecond},
70
        description: "The time spent waiting for a database connection"
71
      ),
72
      summary("openagents.repo.query.idle_time",
73
        unit: {:native, :millisecond},
74
        description:
75
          "The time the connection spent waiting before being checked out for the query"
76
      ),
77
78
      # VM Metrics
79
      summary("vm.memory.total", unit: {:byte, :kilobyte}),
80
      summary("vm.total_run_queue_lengths.total"),
81
      summary("vm.total_run_queue_lengths.cpu"),
82
      summary("vm.total_run_queue_lengths.io")
83
    ]
84
  end
85
86
  defp periodic_measurements do
87
    [
88
      # A module, function and arguments to be invoked periodically.
89
      # This function must call :telemetry.execute/3 and a metric must be added above.
90
      # {OpenAgentsWeb, :count_users, []}
91
    ]
92
  end
93
end
mix.exs added +101

@@ -0,0 +1,101 @@

1
defmodule OpenAgents.MixProject do
2
  use Mix.Project
3
4
  def project do
5
    [
6
      app: :openagents,
7
      version: "0.1.0",
8
      elixir: "~> 1.17",
9
      elixirc_paths: elixirc_paths(Mix.env()),
10
      start_permanent: Mix.env() == :prod,
11
      aliases: aliases(),
12
      deps: deps(),
13
      compilers: [:phoenix_live_view] ++ Mix.compilers(),
14
      listeners: [Phoenix.CodeReloader]
15
    ]
16
  end
17
18
  # Configuration for the OTP application.
19
  #
20
  # Type `mix help compile.app` for more information.
21
  def application do
22
    [
23
      mod: {OpenAgents.Application, []},
24
      extra_applications: [:logger, :runtime_tools]
25
    ]
26
  end
27
28
  def cli do
29
    [
30
      preferred_envs: [precommit: :test]
31
    ]
32
  end
33
34
  # Specifies which paths to compile per environment.
35
  defp elixirc_paths(:test), do: ["lib", "test/support"]
36
  defp elixirc_paths(_), do: ["lib"]
37
38
  # Specifies your project dependencies.
39
  #
40
  # Type `mix help deps` for examples and options.
41
  defp deps do
42
    [
43
      {:phoenix, "~> 1.8.9"},
44
      {:phoenix_ecto, "~> 4.5"},
45
      {:ecto_sql, "~> 3.13"},
46
      {:postgrex, ">= 0.0.0"},
47
      {:phoenix_html, "~> 4.1"},
48
      {:phoenix_live_reload, "~> 1.2", only: :dev},
49
      {:phoenix_live_view, "~> 1.2.0"},
50
      {:lazy_html, ">= 0.1.0", only: :test},
51
      {:phoenix_live_dashboard, "~> 0.8.3"},
52
      {:esbuild, "~> 0.10", runtime: Mix.env() == :dev},
53
      {:tailwind, "~> 0.5", runtime: Mix.env() == :dev},
54
      {:heroicons,
55
       github: "tailwindlabs/heroicons",
56
       tag: "v2.2.0",
57
       sparse: "optimized",
58
       app: false,
59
       compile: false,
60
       depth: 1},
61
      {:daisyui,
62
       github: "saadeghi/daisyui",
63
       tag: "v5.5.20",
64
       sparse: "packages/bundle",
65
       app: false,
66
       compile: false,
67
       depth: 1},
68
      {:swoosh, "~> 1.16"},
69
      {:req, "~> 0.5"},
70
      {:telemetry_metrics, "~> 1.0"},
71
      {:telemetry_poller, "~> 1.0"},
72
      {:gettext, "~> 1.0"},
73
      {:jason, "~> 1.2"},
74
      {:dns_cluster, "~> 0.2.0"},
75
      {:bandit, "~> 1.5"}
76
    ]
77
  end
78
79
  # Aliases are shortcuts or tasks specific to the current project.
80
  # For example, to install project dependencies and perform other setup tasks, run:
81
  #
82
  #     $ mix setup
83
  #
84
  # See the documentation for `Mix` for more info on aliases.
85
  defp aliases do
86
    [
87
      setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
88
      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
89
      "ecto.reset": ["ecto.drop", "ecto.setup"],
90
      test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
91
      "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
92
      "assets.build": ["compile", "tailwind openagents", "esbuild openagents"],
93
      "assets.deploy": [
94
        "tailwind openagents --minify",
95
        "esbuild openagents --minify",
96
        "phx.digest"
97
      ],
98
      precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"]
99
    ]
100
  end
101
end
mix.lock added +46

@@ -0,0 +1,46 @@

1
%{
2
  "bandit": {:hex, :bandit, "1.12.4", "10bbab488edf8162318d736c19c5837077b8fca2bf5d95b07b33830387124f62", [: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.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "84513318c5752a2a8017664450f889b47fae5d53d64698ddf1e4fb09a7449e8d"},
3
  "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
4
  "daisyui": {:git, "https://github.com/saadeghi/daisyui.git", "22ecff57f2c391b80a75617325748cf4d13fdf47", [tag: "v5.5.20", sparse: "packages/bundle", depth: 1]},
5
  "db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"},
6
  "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
7
  "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
8
  "ecto": {:hex, :ecto, "3.14.2", "99db28a864293a789c970651de711e3cae184291e0e7ea1166c54055ac41c1f3", [:mix], [{:decimal, "~> 3.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", "25d60b8c816a07d19d85b80bdf60978bd8b102209dda198d768cd7c6745339a6"},
9
  "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [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", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"},
10
  "elixir_make": {:hex, :elixir_make, "0.10.0", "16577e2583a79bb79237bbff349619ef5d80afffc07eac6e4faf0d00e2ddaf7d", [:mix], [], "hexpm", "dc1f09fb7fa68866b886abd5f0f3c83553b1a19a52359a899e92af1bb3b31982"},
11
  "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
12
  "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
13
  "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
14
  "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"},
15
  "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
16
  "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
17
  "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
18
  "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"},
19
  "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"},
20
  "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
21
  "lazy_html": {:hex, :lazy_html, "0.1.12", "31a55ee622918fce988c94b06232227b42daa64e4eab14ac32081d0f3fd8db6f", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "8a0da594776caee58782c6f93b2abaa5bdb809daf8d43351a561f7de9dc2e2a8"},
22
  "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
23
  "mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"},
24
  "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
25
  "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
26
  "phoenix": {:hex, :phoenix, "1.8.11", "9bb8f0c4e9f0b9eaf7f34cf8cb0bd25bd8599763a5587d64918a9ebb04027823", [: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, "~> 2.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "44f028f4129e5a29487e868f84903373e3d032da151ad0c789c3849f464e7351"},
27
  "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [: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", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
28
  "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
29
  "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"},
30
  "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.7.0", "fb1e429f6d8778ce3a6962debdc5e555428a05a6e7b058d6dbad13d281a2c31f", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "dc9f44271aa6fc4ab7797f2aa374ba096ef2c87520586280eb095626b7387a68"},
31
  "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.9", "d35ddac2fab4480e6bdbf712ff104fd9e969a2bbe81b578ee80f066b371f56db", [: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", [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", "2f9528c3d7046edabbb30a91710ca33988f8d8bc20a964a1fc48b32134572afa"},
32
  "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
33
  "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"},
34
  "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [: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", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"},
35
  "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"},
36
  "postgrex": {:hex, :postgrex, "0.22.4", "d271f595dfd25230b6398354e19d17bb5e2d20130fd2d9bdca7e15f125d43552", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "4aae45a2d60e35b04eea2602440be152fae332901f1fc7a60fc7cb7f0f9a9c5a"},
37
  "req": {:hex, :req, "0.7.3", "b141f1b465dabc5fb8ce67bd2f15a85fc80f6c75719910699560e32ce49f62ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "73b303030dccc2b6d023ee5ada380825ab3a7cd3863aead493db09ec420ffdf2"},
38
  "swoosh": {:hex, :swoosh, "1.27.1", "ca5c1e4a4914c6d1d73d17aaefc3ed53e96edf6b84e0cca03620652ef689cf9d", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9e732f469259e5e84364967b6e0b5d5562adf35bd77576b92a6f3753de6ae9e1"},
39
  "tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"},
40
  "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
41
  "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
42
  "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
43
  "thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"},
44
  "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
45
  "websock_adapter": {:hex, :websock_adapter, "0.6.0", "73db5ab8aaefd1a876a97ce3e6afc96562625de69ef17a4e04426e034849d0b8", [: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", "50021a85bce8f203b086705d9e0c5415e2c7eb05d319111b0428fe71f9934617"},
46
}
priv/gettext/en/LC_MESSAGES/errors.po added +112

@@ -0,0 +1,112 @@

1
## `msgid`s in this file come from POT (.pot) files.
2
##
3
## Do not add, change, or remove `msgid`s manually here as
4
## they're tied to the ones in the corresponding POT file
5
## (with the same domain).
6
##
7
## Use `mix gettext.extract --merge` or `mix gettext.merge`
8
## to merge POT files into PO files.
9
msgid ""
10
msgstr ""
11
"Language: en\n"
12
13
## From Ecto.Changeset.cast/4
14
msgid "can't be blank"
15
msgstr ""
16
17
## From Ecto.Changeset.unique_constraint/3
18
msgid "has already been taken"
19
msgstr ""
20
21
## From Ecto.Changeset.put_change/3
22
msgid "is invalid"
23
msgstr ""
24
25
## From Ecto.Changeset.validate_acceptance/3
26
msgid "must be accepted"
27
msgstr ""
28
29
## From Ecto.Changeset.validate_format/3
30
msgid "has invalid format"
31
msgstr ""
32
33
## From Ecto.Changeset.validate_subset/3
34
msgid "has an invalid entry"
35
msgstr ""
36
37
## From Ecto.Changeset.validate_exclusion/3
38
msgid "is reserved"
39
msgstr ""
40
41
## From Ecto.Changeset.validate_confirmation/3
42
msgid "does not match confirmation"
43
msgstr ""
44
45
## From Ecto.Changeset.no_assoc_constraint/3
46
msgid "is still associated with this entry"
47
msgstr ""
48
49
msgid "are still associated with this entry"
50
msgstr ""
51
52
## From Ecto.Changeset.validate_length/3
53
msgid "should have %{count} item(s)"
54
msgid_plural "should have %{count} item(s)"
55
msgstr[0] ""
56
msgstr[1] ""
57
58
msgid "should be %{count} character(s)"
59
msgid_plural "should be %{count} character(s)"
60
msgstr[0] ""
61
msgstr[1] ""
62
63
msgid "should be %{count} byte(s)"
64
msgid_plural "should be %{count} byte(s)"
65
msgstr[0] ""
66
msgstr[1] ""
67
68
msgid "should have at least %{count} item(s)"
69
msgid_plural "should have at least %{count} item(s)"
70
msgstr[0] ""
71
msgstr[1] ""
72
73
msgid "should be at least %{count} character(s)"
74
msgid_plural "should be at least %{count} character(s)"
75
msgstr[0] ""
76
msgstr[1] ""
77
78
msgid "should be at least %{count} byte(s)"
79
msgid_plural "should be at least %{count} byte(s)"
80
msgstr[0] ""
81
msgstr[1] ""
82
83
msgid "should have at most %{count} item(s)"
84
msgid_plural "should have at most %{count} item(s)"
85
msgstr[0] ""
86
msgstr[1] ""
87
88
msgid "should be at most %{count} character(s)"
89
msgid_plural "should be at most %{count} character(s)"
90
msgstr[0] ""
91
msgstr[1] ""
92
93
msgid "should be at most %{count} byte(s)"
94
msgid_plural "should be at most %{count} byte(s)"
95
msgstr[0] ""
96
msgstr[1] ""
97
98
## From Ecto.Changeset.validate_number/3
99
msgid "must be less than %{number}"
100
msgstr ""
101
102
msgid "must be greater than %{number}"
103
msgstr ""
104
105
msgid "must be less than or equal to %{number}"
106
msgstr ""
107
108
msgid "must be greater than or equal to %{number}"
109
msgstr ""
110
111
msgid "must be equal to %{number}"
112
msgstr ""
priv/gettext/errors.pot added +109

@@ -0,0 +1,109 @@

1
## This is a PO Template file.
2
##
3
## `msgid`s here are often extracted from source code.
4
## Add new translations manually only if they're dynamic
5
## translations that can't be statically extracted.
6
##
7
## Run `mix gettext.extract` to bring this file up to
8
## date. Leave `msgstr`s empty as changing them here has no
9
## effect: edit them in PO (`.po`) files instead.
10
## From Ecto.Changeset.cast/4
11
msgid "can't be blank"
12
msgstr ""
13
14
## From Ecto.Changeset.unique_constraint/3
15
msgid "has already been taken"
16
msgstr ""
17
18
## From Ecto.Changeset.put_change/3
19
msgid "is invalid"
20
msgstr ""
21
22
## From Ecto.Changeset.validate_acceptance/3
23
msgid "must be accepted"
24
msgstr ""
25
26
## From Ecto.Changeset.validate_format/3
27
msgid "has invalid format"
28
msgstr ""
29
30
## From Ecto.Changeset.validate_subset/3
31
msgid "has an invalid entry"
32
msgstr ""
33
34
## From Ecto.Changeset.validate_exclusion/3
35
msgid "is reserved"
36
msgstr ""
37
38
## From Ecto.Changeset.validate_confirmation/3
39
msgid "does not match confirmation"
40
msgstr ""
41
42
## From Ecto.Changeset.no_assoc_constraint/3
43
msgid "is still associated with this entry"
44
msgstr ""
45
46
msgid "are still associated with this entry"
47
msgstr ""
48
49
## From Ecto.Changeset.validate_length/3
50
msgid "should have %{count} item(s)"
51
msgid_plural "should have %{count} item(s)"
52
msgstr[0] ""
53
msgstr[1] ""
54
55
msgid "should be %{count} character(s)"
56
msgid_plural "should be %{count} character(s)"
57
msgstr[0] ""
58
msgstr[1] ""
59
60
msgid "should be %{count} byte(s)"
61
msgid_plural "should be %{count} byte(s)"
62
msgstr[0] ""
63
msgstr[1] ""
64
65
msgid "should have at least %{count} item(s)"
66
msgid_plural "should have at least %{count} item(s)"
67
msgstr[0] ""
68
msgstr[1] ""
69
70
msgid "should be at least %{count} character(s)"
71
msgid_plural "should be at least %{count} character(s)"
72
msgstr[0] ""
73
msgstr[1] ""
74
75
msgid "should be at least %{count} byte(s)"
76
msgid_plural "should be at least %{count} byte(s)"
77
msgstr[0] ""
78
msgstr[1] ""
79
80
msgid "should have at most %{count} item(s)"
81
msgid_plural "should have at most %{count} item(s)"
82
msgstr[0] ""
83
msgstr[1] ""
84
85
msgid "should be at most %{count} character(s)"
86
msgid_plural "should be at most %{count} character(s)"
87
msgstr[0] ""
88
msgstr[1] ""
89
90
msgid "should be at most %{count} byte(s)"
91
msgid_plural "should be at most %{count} byte(s)"
92
msgstr[0] ""
93
msgstr[1] ""
94
95
## From Ecto.Changeset.validate_number/3
96
msgid "must be less than %{number}"
97
msgstr ""
98
99
msgid "must be greater than %{number}"
100
msgstr ""
101
102
msgid "must be less than or equal to %{number}"
103
msgstr ""
104
105
msgid "must be greater than or equal to %{number}"
106
msgstr ""
107
108
msgid "must be equal to %{number}"
109
msgstr ""
priv/repo/migrations/.formatter.exs added +4

@@ -0,0 +1,4 @@

1
[
2
  import_deps: [:ecto_sql],
3
  inputs: ["*.exs"]
4
]
priv/repo/seeds.exs added +11

@@ -0,0 +1,11 @@

1
# Script for populating the database. You can run it as:
2
#
3
#     mix run priv/repo/seeds.exs
4
#
5
# Inside the script, you can read and write to any of your
6
# repositories directly:
7
#
8
#     OpenAgents.Repo.insert!(%OpenAgents.SomeSchema{})
9
#
10
# We recommend using the bang functions (`insert!`, `update!`
11
# and so on) as they will fail if something goes wrong.
priv/static/favicon.ico added

Binary file. Nothing to show as text.

priv/static/images/logo.svg added +6

@@ -0,0 +1,6 @@

1
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" fill="currentColor" aria-hidden="true">
2
  <path
3
    d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.077.057c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728a13 13 0 0 0 1.182 1.106c1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zl-.006.006-.036-.004.021.018.012.053Za.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zl-.008.01.005.026.024.014Z"
4
    fill="#FD4F00"
5
  />
6
</svg>
priv/static/robots.txt added +5

@@ -0,0 +1,5 @@

1
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2
#
3
# To ban all spiders from the entire site uncomment the next two lines:
4
# User-agent: *
5
# Disallow: /
test/openagents_web/controllers/error_html_test.exs added +14

@@ -0,0 +1,14 @@

1
defmodule OpenAgentsWeb.ErrorHTMLTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  # Bring render_to_string/4 for testing custom views
5
  import Phoenix.Template, only: [render_to_string: 4]
6
7
  test "renders 404.html" do
8
    assert render_to_string(OpenAgentsWeb.ErrorHTML, "404", "html", []) == "Not Found"
9
  end
10
11
  test "renders 500.html" do
12
    assert render_to_string(OpenAgentsWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
13
  end
14
end
test/openagents_web/controllers/error_json_test.exs added +12

@@ -0,0 +1,12 @@

1
defmodule OpenAgentsWeb.ErrorJSONTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  test "renders 404" do
5
    assert OpenAgentsWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
6
  end
7
8
  test "renders 500" do
9
    assert OpenAgentsWeb.ErrorJSON.render("500.json", %{}) ==
10
             %{errors: %{detail: "Internal Server Error"}}
11
  end
12
end
test/openagents_web/controllers/page_controller_test.exs added +8

@@ -0,0 +1,8 @@

1
defmodule OpenAgentsWeb.PageControllerTest do
2
  use OpenAgentsWeb.ConnCase
3
4
  test "GET /", %{conn: conn} do
5
    conn = get(conn, ~p"/")
6
    assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
7
  end
8
end
test/support/conn_case.ex added +38

@@ -0,0 +1,38 @@

1
defmodule OpenAgentsWeb.ConnCase do
2
  @moduledoc """
3
  This module defines the test case to be used by
4
  tests that require setting up a connection.
5
6
  Such tests rely on `Phoenix.ConnTest` and also
7
  import other functionality to make it easier
8
  to build common data structures and query the data layer.
9
10
  Finally, if the test case interacts with the database,
11
  we enable the SQL sandbox, so changes done to the database
12
  are reverted at the end of every test. If you are using
13
  PostgreSQL, you can even run database tests asynchronously
14
  by setting `use OpenAgentsWeb.ConnCase, async: true`, although
15
  this option is not recommended for other databases.
16
  """
17
18
  use ExUnit.CaseTemplate
19
20
  using do
21
    quote do
22
      # The default endpoint for testing
23
      @endpoint OpenAgentsWeb.Endpoint
24
25
      use OpenAgentsWeb, :verified_routes
26
27
      # Import conveniences for testing with connections
28
      import Plug.Conn
29
      import Phoenix.ConnTest
30
      import OpenAgentsWeb.ConnCase
31
    end
32
  end
33
34
  setup tags do
35
    OpenAgents.DataCase.setup_sandbox(tags)
36
    {:ok, conn: Phoenix.ConnTest.build_conn()}
37
  end
38
end
test/support/data_case.ex added +58

@@ -0,0 +1,58 @@

1
defmodule OpenAgents.DataCase do
2
  @moduledoc """
3
  This module defines the setup for tests requiring
4
  access to the application's data layer.
5
6
  You may define functions here to be used as helpers in
7
  your tests.
8
9
  Finally, if the test case interacts with the database,
10
  we enable the SQL sandbox, so changes done to the database
11
  are reverted at the end of every test. If you are using
12
  PostgreSQL, you can even run database tests asynchronously
13
  by setting `use OpenAgents.DataCase, async: true`, although
14
  this option is not recommended for other databases.
15
  """
16
17
  use ExUnit.CaseTemplate
18
19
  using do
20
    quote do
21
      alias OpenAgents.Repo
22
23
      import Ecto
24
      import Ecto.Changeset
25
      import Ecto.Query
26
      import OpenAgents.DataCase
27
    end
28
  end
29
30
  setup tags do
31
    OpenAgents.DataCase.setup_sandbox(tags)
32
    :ok
33
  end
34
35
  @doc """
36
  Sets up the sandbox based on the test tags.
37
  """
38
  def setup_sandbox(tags) do
39
    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(OpenAgents.Repo, shared: not tags[:async])
40
    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
41
  end
42
43
  @doc """
44
  A helper that transforms changeset errors into a map of messages.
45
46
      assert {:error, changeset} = Accounts.create_user(%{password: "short"})
47
      assert "password is too short" in errors_on(changeset).password
48
      assert %{password: ["password is too short"]} = errors_on(changeset)
49
50
  """
51
  def errors_on(changeset) do
52
    Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
53
      Regex.replace(~r"%{(\w+)}", message, fn _, key ->
54
        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
55
      end)
56
    end)
57
  end
58
end
test/test_helper.exs added +2

@@ -0,0 +1,2 @@

1
ExUnit.start()
2
Ecto.Adapters.SQL.Sandbox.mode(OpenAgents.Repo, :manual)

This page updates live while a promote is in flight · changelog