diff --git a/.gitignore b/.gitignore
index 5b98ee16..4178c6a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,7 +25,7 @@ cannery-*.tar
# If NPM crashes, it generates a log, let's ignore it too.
npm-debug.log
-# The directory NPM downloads your dependencies sources to.
+# Ignore assets that are produced by build tools.
/assets/node_modules/
# Since we are building assets from assets/,
@@ -36,4 +36,4 @@ npm-debug.log
.elixir_ls/
# direnv
-.envrc
\ No newline at end of file
+.envrc
diff --git a/README.md b/README.md
index ee176a50..12ae810a 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# Cannery is a personal ammo manager that adjusts to your own needs.
-* Easy to Use: Cannery lets you easily keep an eye on your ammo levels after range day
+* Easy to Use: Cannery lets you easily keep an eye on your ammo levels before and after range day
* Secure: Self-host your own instance, or use an instance from someone you trust.
-* Simple: Access from any internet-capable device
\ No newline at end of file
+* Simple: Access from any internet-capable device
diff --git a/assets/css/app.scss b/assets/css/app.scss
index d062fa68..cb858968 100644
--- a/assets/css/app.scss
+++ b/assets/css/app.scss
@@ -10,25 +10,7 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
@import "components";
-/* LiveView specific classes for your customizations */
-.phx-no-feedback.invalid-feedback,
-.phx-no-feedback .invalid-feedback {
- display: none;
-}
-
-.phx-click-loading {
- opacity: 0.5;
- transition: opacity 1s ease-out;
-}
-
-.phx-disconnected{
- cursor: wait;
-}
-.phx-disconnected *{
- pointer-events: none;
-}
-
-/* Alerts and form errors */
+/* Alerts and form errors used by phx.new */
.alert {
padding: 15px;
margin-bottom: 20px;
@@ -60,4 +42,88 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
color: #a94442;
display: block;
margin: -1rem 0 2rem;
-}
\ No newline at end of file
+}
+
+/* LiveView specific classes for your customization */
+.phx-no-feedback.invalid-feedback,
+.phx-no-feedback .invalid-feedback {
+ display: none;
+}
+
+.phx-click-loading {
+ opacity: 0.5;
+ transition: opacity 1s ease-out;
+}
+
+.phx-loading{
+ cursor: wait;
+}
+
+.phx-modal {
+ opacity: 1!important;
+ position: fixed;
+ z-index: 1;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0,0,0,0.4);
+}
+
+.phx-modal-content {
+ background-color: #fefefe;
+ margin: 15vh auto;
+ padding: 20px;
+ border: 1px solid #888;
+ width: 80%;
+}
+
+.phx-modal-close {
+ color: #aaa;
+ float: right;
+ font-size: 28px;
+ font-weight: bold;
+}
+
+.phx-modal-close:hover,
+.phx-modal-close:focus {
+ color: black;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.fade-in-scale {
+ animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys;
+}
+
+.fade-out-scale {
+ animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
+}
+
+.fade-in {
+ animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
+}
+.fade-out {
+ animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
+}
+
+@keyframes fade-in-scale-keys{
+ 0% { scale: 0.95; opacity: 0; }
+ 100% { scale: 1.0; opacity: 1; }
+}
+
+@keyframes fade-out-scale-keys{
+ 0% { scale: 1.0; opacity: 1; }
+ 100% { scale: 0.95; opacity: 0; }
+}
+
+@keyframes fade-in-keys{
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+
+@keyframes fade-out-keys{
+ 0% { opacity: 1; }
+ 100% { opacity: 0; }
+}
diff --git a/assets/js/app.js b/assets/js/app.js
index b1528d7e..85814181 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -1,31 +1,33 @@
-// We need to import the CSS so that webpack will load it.
-// The MiniCssExtractPlugin is used to separate it out into
-// its own CSS file.
+// We import the CSS which is extracted to its own file by esbuild.
+// Remove this line if you add a your own CSS build pipeline (e.g postcss).
import "../css/app.scss"
-// webpack automatically bundles all modules in your
-// entry points. Those entry points can be configured
-// in "webpack.config.js".
-//
-// Import deps with the dep name or local files with a relative path, for example:
-//
-// import {Socket} from "phoenix"
-// import socket from "./socket"
-//
-import "phoenix_html"
-import {Socket} from "phoenix"
-import topbar from "topbar"
-import {LiveSocket} from "phoenix_live_view"
+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
+// to get started and then uncomment the line below.
+// import "./user_socket.js"
-let Hooks = {};
-Hooks.MaintainAttrs = {
- attrs(){ return this.el.getAttribute("data-attrs").split(", ") },
- beforeUpdate(){ this.prevAttrs = this.attrs().map(name => [name, this.el.getAttribute(name)]) },
- updated(){ this.prevAttrs.forEach(([name, val]) => this.el.setAttribute(name, val)) }
-};
+// You can include dependencies in two ways.
+//
+// The simplest option is to put them in assets/vendor and
+// import them using relative paths:
+//
+// import "../vendor/some-package.js"
+//
+// Alternatively, you can `npm install some-package --prefix assets` and import
+// them using a path starting with the package name:
+//
+// import "some-package"
+//
+
+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
+import "phoenix_html"
+// Establish Phoenix Socket and LiveView configuration.
+import {Socket} from "phoenix"
+import {LiveSocket} from "phoenix_live_view"
+import topbar from "../vendor/topbar"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
-let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})
+let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
@@ -40,4 +42,3 @@ liveSocket.connect()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket
-
diff --git a/assets/js/socket.js b/assets/js/socket.js
deleted file mode 100644
index 8a673927..00000000
--- a/assets/js/socket.js
+++ /dev/null
@@ -1,63 +0,0 @@
-// NOTE: The contents of this file will only be executed if
-// you uncomment its entry in "assets/js/app.js".
-
-// To use Phoenix channels, the first step is to import Socket,
-// and connect at the socket path in "lib/web/endpoint.ex".
-//
-// Pass the token on params as below. Or remove it
-// from the params if you are not using authentication.
-import {Socket} from "phoenix"
-
-let socket = new Socket("/socket", {params: {token: window.userToken}})
-
-// When you connect, you'll often need to authenticate the client.
-// For example, imagine you have an authentication plug, `MyAuth`,
-// which authenticates the session and assigns a `:current_user`.
-// If the current user exists you can assign the user's token in
-// the connection for use in the layout.
-//
-// In your "lib/web/router.ex":
-//
-// pipeline :browser do
-// ...
-// plug MyAuth
-// plug :put_user_token
-// end
-//
-// defp put_user_token(conn, _) do
-// if current_user = conn.assigns[:current_user] do
-// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
-// assign(conn, :user_token, token)
-// else
-// conn
-// end
-// end
-//
-// Now you need to pass this token to JavaScript. You can do so
-// inside a script tag in "lib/web/templates/layout/app.html.eex":
-//
-//
-//
-// You will need to verify the user token in the "connect/3" function
-// in "lib/web/channels/user_socket.ex":
-//
-// def connect(%{"token" => token}, socket, _connect_info) do
-// # max_age: 1209600 is equivalent to two weeks in seconds
-// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
-// {:ok, user_id} ->
-// {:ok, socket |> assign(:user, user_id)}
-// {:error, reason} ->
-// :error
-// end
-// end
-//
-// Finally, connect to the socket:
-socket.connect()
-
-// Now that you are connected, you can join channels with a topic:
-let channel = socket.channel("topic:subtopic", {})
-channel.join()
- .receive("ok", resp => { console.log("Joined successfully", resp) })
- .receive("error", resp => { console.log("Unable to join", resp) })
-
-export default socket
diff --git a/assets/package-lock.json b/assets/package-lock.json
index c7514b75..69549de0 100644
--- a/assets/package-lock.json
+++ b/assets/package-lock.json
@@ -1109,9 +1109,9 @@
"dev": true
},
"@types/eslint": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.0.tgz",
- "integrity": "sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A==",
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.0.tgz",
+ "integrity": "sha512-JUYa/5JwoqikCy7O7jKtuNe9Z4ZZt615G+1EKfaDGSNEpzaA2OwbV/G1v08Oa7fd1XzlFoSCvt9ePl9/6FyAug==",
"dev": true,
"requires": {
"@types/estree": "*",
@@ -1119,9 +1119,9 @@
}
},
"@types/eslint-scope": {
- "version": "3.7.1",
- "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz",
- "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==",
+ "version": "3.7.3",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz",
+ "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==",
"dev": true,
"requires": {
"@types/eslint": "*",
@@ -1382,9 +1382,9 @@
"dev": true
},
"acorn-import-assertions": {
- "version": "1.7.6",
- "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz",
- "integrity": "sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
+ "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
"dev": true
},
"acorn-node": {
@@ -3190,9 +3190,9 @@
}
},
"enhanced-resolve": {
- "version": "5.8.2",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
- "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==",
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz",
+ "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.4",
@@ -3200,9 +3200,9 @@
},
"dependencies": {
"tapable": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
- "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"dev": true
}
}
@@ -3244,9 +3244,9 @@
}
},
"es-module-lexer": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.7.1.tgz",
- "integrity": "sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw==",
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
+ "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
"dev": true
},
"escalade": {
@@ -3287,9 +3287,9 @@
},
"dependencies": {
"estraverse": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
- "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true
}
}
@@ -9401,9 +9401,9 @@
}
},
"watchpack": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz",
- "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
+ "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
"dev": true,
"requires": {
"glob-to-regexp": "^0.4.1",
@@ -9420,9 +9420,9 @@
}
},
"webpack": {
- "version": "5.50.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.50.0.tgz",
- "integrity": "sha512-hqxI7t/KVygs0WRv/kTgUW8Kl3YC81uyWQSo/7WUs5LsuRw0htH/fCwbVBGCuiX/t4s7qzjXFcf41O8Reiypag==",
+ "version": "5.67.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.67.0.tgz",
+ "integrity": "sha512-LjFbfMh89xBDpUMgA1W9Ur6Rn/gnr2Cq1jjHFPo4v6a79/ypznSYbAyPgGhwsxBtMIaEmDD1oJoA7BEYw/Fbrw==",
"dev": true,
"requires": {
"@types/eslint-scope": "^3.7.0",
@@ -9434,12 +9434,12 @@
"acorn-import-assertions": "^1.7.6",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.8.0",
- "es-module-lexer": "^0.7.1",
+ "enhanced-resolve": "^5.8.3",
+ "es-module-lexer": "^0.9.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
- "graceful-fs": "^4.2.4",
+ "graceful-fs": "^4.2.9",
"json-parse-better-errors": "^1.0.2",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
@@ -9447,14 +9447,20 @@
"schema-utils": "^3.1.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.1.3",
- "watchpack": "^2.2.0",
- "webpack-sources": "^3.2.0"
+ "watchpack": "^2.3.1",
+ "webpack-sources": "^3.2.3"
},
"dependencies": {
"acorn": {
- "version": "8.4.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz",
- "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==",
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
+ "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
+ "dev": true
+ },
+ "graceful-fs": {
+ "version": "4.2.9",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
+ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
"dev": true
},
"schema-utils": {
@@ -9469,15 +9475,15 @@
}
},
"tapable": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
- "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"dev": true
},
"webpack-sources": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.0.tgz",
- "integrity": "sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==",
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
+ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true
}
}
diff --git a/assets/package.json b/assets/package.json
index ed934c18..052183ca 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -33,7 +33,7 @@
"style-loader": "^3.2.1",
"tailwindcss": "^2.2.7",
"terser-webpack-plugin": "^5.1.3",
- "webpack": "^5.50.0",
+ "webpack": "^5.67.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^3.11.2"
}
diff --git a/assets/static/robots.txt b/assets/static/robots.txt
index 3c9c7c01..26e06b5f 100644
--- a/assets/static/robots.txt
+++ b/assets/static/robots.txt
@@ -1,4 +1,4 @@
-# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js
new file mode 100644
index 00000000..1f622097
--- /dev/null
+++ b/assets/vendor/topbar.js
@@ -0,0 +1,157 @@
+/**
+ * @license MIT
+ * topbar 1.0.0, 2021-01-06
+ * https://buunguyen.github.io/topbar
+ * Copyright (c) 2021 Buu Nguyen
+ */
+(function (window, document) {
+ "use strict";
+
+ // https://gist.github.com/paulirish/1579671
+ (function () {
+ var lastTime = 0;
+ var vendors = ["ms", "moz", "webkit", "o"];
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame =
+ window[vendors[x] + "RequestAnimationFrame"];
+ window.cancelAnimationFrame =
+ window[vendors[x] + "CancelAnimationFrame"] ||
+ window[vendors[x] + "CancelRequestAnimationFrame"];
+ }
+ if (!window.requestAnimationFrame)
+ window.requestAnimationFrame = function (callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function () {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ if (!window.cancelAnimationFrame)
+ window.cancelAnimationFrame = function (id) {
+ clearTimeout(id);
+ };
+ })();
+
+ var canvas,
+ progressTimerId,
+ fadeTimerId,
+ currentProgress,
+ showing,
+ addEvent = function (elem, type, handler) {
+ if (elem.addEventListener) elem.addEventListener(type, handler, false);
+ else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
+ else elem["on" + type] = handler;
+ },
+ options = {
+ autoRun: true,
+ barThickness: 3,
+ barColors: {
+ 0: "rgba(26, 188, 156, .9)",
+ ".25": "rgba(52, 152, 219, .9)",
+ ".50": "rgba(241, 196, 15, .9)",
+ ".75": "rgba(230, 126, 34, .9)",
+ "1.0": "rgba(211, 84, 0, .9)",
+ },
+ shadowBlur: 10,
+ shadowColor: "rgba(0, 0, 0, .6)",
+ className: null,
+ },
+ repaint = function () {
+ canvas.width = window.innerWidth;
+ canvas.height = options.barThickness * 5; // need space for shadow
+
+ var ctx = canvas.getContext("2d");
+ ctx.shadowBlur = options.shadowBlur;
+ ctx.shadowColor = options.shadowColor;
+
+ var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
+ for (var stop in options.barColors)
+ lineGradient.addColorStop(stop, options.barColors[stop]);
+ ctx.lineWidth = options.barThickness;
+ ctx.beginPath();
+ ctx.moveTo(0, options.barThickness / 2);
+ ctx.lineTo(
+ Math.ceil(currentProgress * canvas.width),
+ options.barThickness / 2
+ );
+ ctx.strokeStyle = lineGradient;
+ ctx.stroke();
+ },
+ createCanvas = function () {
+ canvas = document.createElement("canvas");
+ var style = canvas.style;
+ style.position = "fixed";
+ style.top = style.left = style.right = style.margin = style.padding = 0;
+ style.zIndex = 100001;
+ style.display = "none";
+ if (options.className) canvas.classList.add(options.className);
+ document.body.appendChild(canvas);
+ addEvent(window, "resize", repaint);
+ },
+ topbar = {
+ config: function (opts) {
+ for (var key in opts)
+ if (options.hasOwnProperty(key)) options[key] = opts[key];
+ },
+ show: function () {
+ if (showing) return;
+ showing = true;
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
+ if (!canvas) createCanvas();
+ canvas.style.opacity = 1;
+ canvas.style.display = "block";
+ topbar.progress(0);
+ if (options.autoRun) {
+ (function loop() {
+ progressTimerId = window.requestAnimationFrame(loop);
+ topbar.progress(
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
+ );
+ })();
+ }
+ },
+ progress: function (to) {
+ if (typeof to === "undefined") return currentProgress;
+ if (typeof to === "string") {
+ to =
+ (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
+ ? currentProgress
+ : 0) + parseFloat(to);
+ }
+ currentProgress = to > 1 ? 1 : to;
+ repaint();
+ return currentProgress;
+ },
+ hide: function () {
+ if (!showing) return;
+ showing = false;
+ if (progressTimerId != null) {
+ window.cancelAnimationFrame(progressTimerId);
+ progressTimerId = null;
+ }
+ (function loop() {
+ if (topbar.progress("+.1") >= 1) {
+ canvas.style.opacity -= 0.05;
+ if (canvas.style.opacity <= 0.05) {
+ canvas.style.display = "none";
+ fadeTimerId = null;
+ return;
+ }
+ }
+ fadeTimerId = window.requestAnimationFrame(loop);
+ })();
+ },
+ };
+
+ if (typeof module === "object" && typeof module.exports === "object") {
+ module.exports = topbar;
+ } else if (typeof define === "function" && define.amd) {
+ define(function () {
+ return topbar;
+ });
+ } else {
+ this.topbar = topbar;
+ }
+}.call(this, window, document));
diff --git a/config/config.exs b/config/config.exs
index 7308f0a7..e6aba003 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,5 +1,5 @@
# This file is responsible for configuring your application
-# and its dependencies with the aid of the Mix.Config module.
+# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
@@ -8,7 +8,8 @@
import Config
config :cannery,
- ecto_repos: [Cannery.Repo]
+ ecto_repos: [Cannery.Repo],
+ generators: [binary_id: true]
# Configures the endpoint
config :cannery, CanneryWeb.Endpoint,
@@ -25,6 +26,28 @@ config :cannery, :generators,
binary_id: true,
sample_binary_id: "11111111-1111-1111-1111-111111111111"
+# Configures the mailer
+#
+# By default it uses the "Local" adapter which stores the emails
+# locally. You can see the emails in your browser, at "/dev/mailbox".
+#
+# For production it's recommended to configure a different adapter
+# at the `config/runtime.exs`.
+config :cannery, Cannery.Mailer, adapter: Swoosh.Adapters.Local
+
+# Swoosh API client is needed for adapters other than SMTP.
+config :swoosh, :api_client, false
+
+# Configure esbuild (the version is required)
+# config :esbuild,
+# version: "0.14.0",
+# default: [
+# args:
+# ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
+# cd: Path.expand("../assets", __DIR__),
+# env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
+# ]
+
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
diff --git a/config/dev.exs b/config/dev.exs
index c2279a98..d734ded1 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -5,6 +5,7 @@ config :cannery, Cannery.Repo,
url:
System.get_env("DATABASE_URL") ||
"ecto://postgres:postgres@localhost/cannery_dev",
+ show_sensitive_data_on_connection_error: true,
pool_size: 10
# For development, we disable any cache and enable
@@ -12,12 +13,18 @@ config :cannery, Cannery.Repo,
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
-# with webpack to recompile .js and .css sources.
+# with esbuild to bundle .js and .css sources.
config :cannery, CanneryWeb.Endpoint,
- debug_errors: true,
- code_reloader: true,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {0, 0, 0, 0}, port: 4000],
check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "dg2lccMgaY3+ZeKppR+ondk4ZRaANZGIN0LMZT1u1uzscH4jO5W9a9b9V9BkC+MW",
watchers: [
+ # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
+ # esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
node: [
"node_modules/webpack/bin/webpack.js",
"--mode",
@@ -58,8 +65,8 @@ config :cannery, CanneryWeb.Endpoint,
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
- ~r"lib/cannery_web/.*(live|views)/.*(ex|leex)$",
- ~r"lib/cannery_web/templates/.*(eex|leex)$"
+ ~r"lib/cannery_web/(live|views)/.*(ex)$",
+ ~r"lib/cannery_web/templates/.*(eex)$"
]
]
diff --git a/config/prod.exs b/config/prod.exs
index ce7b7a36..978ffd15 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -25,14 +25,14 @@ config :logger, level: :info
# to the previous section and set your `:url` port to 443:
#
# config :cannery, CanneryWeb.Endpoint,
-# ...
-# url: [host: "localhost", port: 443],
+# ...,
+# url: [host: "example.com", port: 443],
# https: [
+# ...,
# port: 443,
# cipher_suite: :strong,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
-# certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
-# transport_options: [socket_opts: [:inet6]]
+# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
# ]
#
# The `cipher_suite` is set to `:strong` to support only the
diff --git a/config/releases.exs b/config/releases.exs
deleted file mode 100644
index 155b4cf4..00000000
--- a/config/releases.exs
+++ /dev/null
@@ -1,33 +0,0 @@
-# In this file, we load production configuration and secrets
-# from environment variables. You can also hardcode secrets,
-# although such is generally not recommended and you have to
-# remember to add this file to your .gitignore.
-import Config
-
-database_url =
- System.get_env("DATABASE_URL") ||
- "ecto://postgres:postgres@cannery-db/cannery"
-
-config :cannery, Cannery.Repo,
- # ssl: true,
- url: database_url,
- pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
-
-secret_key_base =
- System.get_env("SECRET_KEY_BASE") ||
- raise """
- environment variable SECRET_KEY_BASE is missing.
- You can generate one by calling: mix phx.gen.secret
- """
-
-host = System.get_env("HOST") || "localhost"
-
-config :cannery, CanneryWeb.Endpoint,
- url: [scheme: "https", host: host, port: "443"],
- http: [
- port: String.to_integer(System.get_env("PORT") || "4000"),
- transport_options: [socket_opts: [:inet6]]
- ],
- secret_key_base: secret_key_base,
- server: true,
- registration: System.get_env("REGISTRATION") || "invite"
diff --git a/config/runtime.exs b/config/runtime.exs
new file mode 100644
index 00000000..4265ca4a
--- /dev/null
+++ b/config/runtime.exs
@@ -0,0 +1,83 @@
+import Config
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# Start the phoenix server if environment is set and running in a release
+if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
+ config :cannery, CanneryWeb.Endpoint, server: true
+end
+
+if config_env() == :prod do
+ database_url =
+ System.get_env("DATABASE_URL") ||
+ "ecto://postgres:postgres@cannery-db/cannery"
+
+ maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []
+
+ config :cannery, Cannery.Repo,
+ # ssl: true,
+ url: database_url,
+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
+ socket_options: maybe_ipv6
+
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("HOST") || "localhost"
+
+ config :cannery, CanneryWeb.Endpoint,
+ url: [scheme: "https", host: host, port: 443],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: String.to_integer(System.get_env("PORT") || "4000")
+ ],
+ secret_key_base: secret_key_base,
+ server: true,
+ registration: System.get_env("REGISTRATION") || "invite"
+
+ # ## Using releases
+ #
+ # If you are doing OTP releases, you need to instruct Phoenix
+ # to start each relevant endpoint:
+ #
+ # config :cannery, CanneryWeb.Endpoint, server: true
+ #
+ # Then you can assemble a release by calling `mix release`.
+ # See `mix help release` for more information.
+
+ # ## Configuring the mailer
+ #
+ # In production you need to configure the mailer to use a different adapter.
+ # Also, you may need to configure the Swoosh API client of your choice if you
+ # are not using SMTP. Here is an example of the configuration:
+ #
+ # config :cannery, Cannery.Mailer,
+ # adapter: Swoosh.Adapters.Mailgun,
+ # api_key: System.get_env("MAILGUN_API_KEY"),
+ # domain: System.get_env("MAILGUN_DOMAIN")
+ #
+ # For this example you need include a HTTP client required by Swoosh API client.
+ # Swoosh supports Hackney and Finch out of the box:
+ #
+ # config :swoosh, :api_client, Swoosh.ApiClient.Hackney
+ #
+ # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
+end
diff --git a/config/test.exs b/config/test.exs
index b6e59c72..95b1d2d6 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -11,15 +11,23 @@ config :bcrypt_elixir, :log_rounds, 1
config :cannery, Cannery.Repo,
username: "postgres",
password: "postgres",
- database: "cannery_test#{System.get_env("MIX_TEST_PARTITION")}",
hostname: "localhost",
- pool: Ecto.Adapters.SQL.Sandbox
+ database: "cannery_test#{System.get_env("MIX_TEST_PARTITION")}",
+ pool: Ecto.Adapters.SQL.Sandbox,
+ pool_size: 10
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :cannery, CanneryWeb.Endpoint,
- http: [port: 4002],
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "S3qq9QtUdsFtlYej+HTjAVN95uP5i5tf2sPYINWSQfCKJghFj2B1+wTAoljZyHOK",
server: false
+# In test we don't send emails.
+config :cannery, Cannery.Mailer, adapter: Swoosh.Adapters.Test
+
# Print only warnings and errors during test
config :logger, level: :warn
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/lib/cannery/application.ex b/lib/cannery/application.ex
index c8fba2ef..a47f7973 100644
--- a/lib/cannery/application.ex
+++ b/lib/cannery/application.ex
@@ -5,19 +5,21 @@ defmodule Cannery.Application do
use Application
+ @impl true
def start(_type, _args) do
children = [
# Start the Ecto repository
Cannery.Repo,
- Cannery.Repo.Migrator,
# Start the Telemetry supervisor
CanneryWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Cannery.PubSub},
# Start the Endpoint (http/https)
- CanneryWeb.Endpoint
+ CanneryWeb.Endpoint,
# Start a worker by calling: Cannery.Worker.start_link(arg)
- # {Cannery.Worker, arg}
+ # {Cannery.Worker, arg},
+ # Automatically migrate on start
+ Cannery.Repo.Migrator
]
# See https://hexdocs.pm/elixir/Supervisor.html
@@ -28,6 +30,7 @@ defmodule Cannery.Application do
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
+ @impl true
def config_change(changed, _new, removed) do
CanneryWeb.Endpoint.config_change(changed, removed)
:ok
diff --git a/lib/cannery/mailer.ex b/lib/cannery/mailer.ex
new file mode 100644
index 00000000..b1cad172
--- /dev/null
+++ b/lib/cannery/mailer.ex
@@ -0,0 +1,3 @@
+defmodule Cannery.Mailer do
+ use Swoosh.Mailer, otp_app: :cannery
+end
diff --git a/lib/cannery_web.ex b/lib/cannery_web.ex
index b37dcabe..52391694 100644
--- a/lib/cannery_web.ex
+++ b/lib/cannery_web.ex
@@ -46,7 +46,7 @@ defmodule CanneryWeb do
quote do
use Phoenix.LiveView,
layout: {CanneryWeb.LayoutView, "live.html"}
-
+
unquote(view_helpers())
end
end
@@ -59,6 +59,14 @@ defmodule CanneryWeb do
end
end
+ def component do
+ quote do
+ use Phoenix.Component
+
+ unquote(view_helpers())
+ end
+ end
+
def router do
quote do
use Phoenix.Router
@@ -81,7 +89,7 @@ defmodule CanneryWeb do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
- # Import LiveView helpers (live_render, live_component, live_patch, etc)
+ # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
import Phoenix.LiveView.Helpers
import CanneryWeb.LiveHelpers
diff --git a/lib/cannery_web/channels/user_socket.ex b/lib/cannery_web/channels/user_socket.ex
deleted file mode 100644
index 985278d2..00000000
--- a/lib/cannery_web/channels/user_socket.ex
+++ /dev/null
@@ -1,35 +0,0 @@
-defmodule CanneryWeb.UserSocket do
- use Phoenix.Socket
-
- ## Channels
- # channel "room:*", CanneryWeb.RoomChannel
-
- # Socket params are passed from the client and can
- # be used to verify and authenticate a user. After
- # verification, you can put default assigns into
- # the socket that will be set for all channels, ie
- #
- # {:ok, socket |> assign(:user_id, verified_user_id)}
- #
- # To deny connection, return `:error`.
- #
- # See `Phoenix.Token` documentation for examples in
- # performing token verification on connect.
- @impl true
- def connect(_params, socket, _connect_info) do
- {:ok, socket}
- end
-
- # Socket id's are topics that allow you to identify all sockets for a given user:
- #
- # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
- #
- # Would allow you to broadcast a "disconnect" event and terminate
- # all active sockets and channels for a given user:
- #
- # CanneryWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
- #
- # Returning `nil` makes this socket anonymous.
- @impl true
- def id(_socket), do: nil
-end
diff --git a/lib/cannery_web/controllers/user_auth.ex b/lib/cannery_web/controllers/user_auth.ex
index 602667eb..743fc1c8 100644
--- a/lib/cannery_web/controllers/user_auth.ex
+++ b/lib/cannery_web/controllers/user_auth.ex
@@ -3,6 +3,7 @@ defmodule CanneryWeb.UserAuth do
import Phoenix.Controller
alias Cannery.Accounts
+ alias CanneryWeb.{HomeLive}
alias CanneryWeb.Router.Helpers, as: Routes
# Make the remember me cookie valid for 60 days.
@@ -138,7 +139,7 @@ defmodule CanneryWeb.UserAuth do
|> halt()
end
end
-
+
@doc """
Used for routes that require the user to be an admin.
"""
@@ -149,7 +150,7 @@ defmodule CanneryWeb.UserAuth do
conn
|> put_flash(:error, "You are not authorized to view this page.")
|> maybe_store_return_to()
- |> redirect(to: Routes.home_path(conn, :index))
+ |> redirect(to: Routes.live_path(conn, HomeLive))
|> halt()
end
end
diff --git a/lib/cannery_web/controllers/user_registration_controller.ex b/lib/cannery_web/controllers/user_registration_controller.ex
index f9e844e5..57597f0c 100644
--- a/lib/cannery_web/controllers/user_registration_controller.ex
+++ b/lib/cannery_web/controllers/user_registration_controller.ex
@@ -3,7 +3,7 @@ defmodule CanneryWeb.UserRegistrationController do
alias Cannery.{Accounts, Invites}
alias Cannery.Accounts.User
- alias CanneryWeb.UserAuth
+ alias CanneryWeb.{HomeLive, UserAuth}
def new(conn, %{"invite" => invite_token}) do
invite = Invites.get_invite_by_token(invite_token)
@@ -13,7 +13,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, "Sorry, this invite was not found or expired")
- |> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
+ |> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive))
end
end
@@ -23,7 +23,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, "Sorry, public registration is disabled")
- |> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
+ |> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive))
end
end
@@ -41,7 +41,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, "Sorry, this invite was not found or expired")
- |> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
+ |> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive))
end
end
@@ -51,7 +51,7 @@ defmodule CanneryWeb.UserRegistrationController do
else
conn
|> put_flash(:error, "Sorry, public registration is disabled")
- |> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
+ |> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive))
end
end
diff --git a/lib/cannery_web/controllers/user_settings_controller.ex b/lib/cannery_web/controllers/user_settings_controller.ex
index eb709d70..23aa9abc 100644
--- a/lib/cannery_web/controllers/user_settings_controller.ex
+++ b/lib/cannery_web/controllers/user_settings_controller.ex
@@ -2,7 +2,7 @@ defmodule CanneryWeb.UserSettingsController do
use CanneryWeb, :controller
alias Cannery.Accounts
- alias CanneryWeb.UserAuth
+ alias CanneryWeb.{HomeLive, UserAuth}
plug :assign_email_and_password_changesets
@@ -70,7 +70,7 @@ defmodule CanneryWeb.UserSettingsController do
conn
|> put_flash(:error, "Your account has been deleted")
- |> redirect(to: Routes.home_path(conn, :index))
+ |> redirect(to: Routes.live_path(conn, HomeLive))
else
conn
|> put_flash(:error, "Unable to delete user")
diff --git a/lib/cannery_web/endpoint.ex b/lib/cannery_web/endpoint.ex
index 7b7e4dc6..f26ebf0b 100644
--- a/lib/cannery_web/endpoint.ex
+++ b/lib/cannery_web/endpoint.ex
@@ -7,13 +7,9 @@ defmodule CanneryWeb.Endpoint do
@session_options [
store: :cookie,
key: "_cannery_key",
- signing_salt: "fxAnJltS"
+ signing_salt: "N8eMKwCG"
]
- socket "/socket", CanneryWeb.UserSocket,
- websocket: true,
- longpoll: false
-
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
diff --git a/lib/cannery_web/live/home_live.html.leex b/lib/cannery_web/live/home_live.html.leex
index 18592d6f..045e32fd 100644
--- a/lib/cannery_web/live/home_live.html.leex
+++ b/lib/cannery_web/live/home_live.html.leex
@@ -13,7 +13,7 @@
Easy to Use:
- Cannery lets you easily keep an eye on your ammo levels after range day
+ Cannery lets you easily keep an eye on your ammo levels before and after range day
diff --git a/lib/cannery_web/router.ex b/lib/cannery_web/router.ex
index eaad88b8..f56014e3 100644
--- a/lib/cannery_web/router.ex
+++ b/lib/cannery_web/router.ex
@@ -24,7 +24,7 @@ defmodule CanneryWeb.Router do
scope "/", CanneryWeb do
pipe_through :browser
- live "/", HomeLive, :index
+ live "/", HomeLive
end
## Authentication routes
@@ -94,4 +94,16 @@ defmodule CanneryWeb.Router do
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :confirm
end
+
+ # Enables the Swoosh mailbox preview in development.
+ #
+ # Note that preview only shows emails that were sent by the same
+ # node running the Phoenix server.
+ if Mix.env() == :dev do
+ scope "/dev" do
+ pipe_through :browser
+
+ forward "/mailbox", Plug.Swoosh.MailboxPreview
+ end
+ end
end
diff --git a/lib/cannery_web/telemetry.ex b/lib/cannery_web/telemetry.ex
index b86bf9f0..d38ec235 100644
--- a/lib/cannery_web/telemetry.ex
+++ b/lib/cannery_web/telemetry.ex
@@ -31,11 +31,27 @@ defmodule CanneryWeb.Telemetry do
),
# Database Metrics
- summary("cannery.repo.query.total_time", unit: {:native, :millisecond}),
- summary("cannery.repo.query.decode_time", unit: {:native, :millisecond}),
- summary("cannery.repo.query.query_time", unit: {:native, :millisecond}),
- summary("cannery.repo.query.queue_time", unit: {:native, :millisecond}),
- summary("cannery.repo.query.idle_time", unit: {:native, :millisecond}),
+ summary("cannery.repo.query.total_time",
+ unit: {:native, :millisecond},
+ description: "The sum of the other measurements"
+ ),
+ summary("cannery.repo.query.decode_time",
+ unit: {:native, :millisecond},
+ description: "The time spent decoding the data received from the database"
+ ),
+ summary("cannery.repo.query.query_time",
+ unit: {:native, :millisecond},
+ description: "The time spent executing the query"
+ ),
+ summary("cannery.repo.query.queue_time",
+ unit: {:native, :millisecond},
+ description: "The time spent waiting for a database connection"
+ ),
+ summary("cannery.repo.query.idle_time",
+ unit: {:native, :millisecond},
+ description:
+ "The time the connection spent waiting before being checked out for the query"
+ ),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
diff --git a/lib/cannery_web/templates/layout/app.html.eex b/lib/cannery_web/templates/layout/app.html.heex
similarity index 98%
rename from lib/cannery_web/templates/layout/app.html.eex
rename to lib/cannery_web/templates/layout/app.html.heex
index 69799212..f1f48725 100644
--- a/lib/cannery_web/templates/layout/app.html.eex
+++ b/lib/cannery_web/templates/layout/app.html.heex
@@ -2,20 +2,20 @@
<%= render "topbar.html", assigns %>
-
+
<%= if get_flash(@conn, :info) do %>
<%= get_flash(@conn, :info) %>
<% end %>
-
+
<%= if get_flash(@conn, :error) do %>
<%= get_flash(@conn, :error) %>
<% end %>
-
+
<%= @inner_content %>
diff --git a/lib/cannery_web/templates/layout/live.html.leex b/lib/cannery_web/templates/layout/live.html.heex
similarity index 61%
rename from lib/cannery_web/templates/layout/live.html.leex
rename to lib/cannery_web/templates/layout/live.html.heex
index efd084d7..1beb8a82 100644
--- a/lib/cannery_web/templates/layout/live.html.leex
+++ b/lib/cannery_web/templates/layout/live.html.heex
@@ -1,16 +1,21 @@
-
+
+
<%= live_component CanneryWeb.Live.Component.Topbar, current_user: assigns[:current_user] %>
-
+
<%= if live_flash(@flash, :info) do %>
-
+
<%= live_flash(@flash, :info) %>
<% end %>
-
+
<%= if live_flash(@flash, :error) do %>
-