Merge branch 'new-dashboard'

This commit is contained in:
James Harton 2019-12-06 14:24:55 +13:00
commit 4fbd8507a0
25 changed files with 1836 additions and 414 deletions

View file

@ -9,7 +9,7 @@ WORKDIR /app
RUN mix deps.get
RUN mix deps.compile
FROM balenalib/%%BALENA_MACHINE_NAME%%-alpine-node:latest as assets
FROM balenalib/%%BALENA_MACHINE_NAME%%-alpine-node:latest-build as assets
COPY --from=deps /app /app
COPY assets /app/assets
WORKDIR /app

View file

@ -1,4 +0,0 @@
/* This file is for your main application css. */
@import "./phoenix.css";
@import "../../deps/phoenix_live_view/assets/css/live_view.css";

View file

@ -0,0 +1,7 @@
/* This file is for your main application css. */
// import "./phoenix.css";
// import "../../deps/phoenix_live_view/assets/css/live_view.css";
@import "~foundation-sites/scss/foundation";
@include foundation-everything();

View file

@ -1,7 +1,7 @@
// 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.
import css from "../css/app.css"
import css from "../css/app.scss"
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
@ -21,3 +21,10 @@ import LiveSocket from "phoenix_live_view"
let liveSocket = new LiveSocket("/live", Socket)
liveSocket.connect()
import jQuery from "jquery";
window.$ = jQuery;
import "what-input"
import "foundation-sites/js/foundation"
$(document).foundation();

View file

@ -6,20 +6,25 @@
"watch": "webpack --mode development --watch"
},
"dependencies": {
"foundation-sites": "^6.5.3",
"jquery": "^3.4.1",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view"
"phoenix_live_view": "file:../deps/phoenix_live_view",
"what-input": "^5.2.6"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-loader": "^8.0.0",
"copy-webpack-plugin": "^4.5.0",
"css-loader": "^2.1.1",
"mini-css-extract-plugin": "^0.4.0",
"copy-webpack-plugin": "^5.0.5",
"css-loader": "^3.2.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.13.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"terser-webpack-plugin": "^1.1.0",
"webpack": "4.4.0",
"sass-loader": "^8.0.0",
"terser-webpack-plugin": "^2.2.1",
"webpack": "4.41.2",
"webpack-cli": "^3.3.2"
}
}

View file

@ -20,8 +20,7 @@ module.exports = (env, options) => ({
path: path.resolve(__dirname, '../priv/static/js')
},
module: {
rules: [
{
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: {
@ -29,8 +28,8 @@ module.exports = (env, options) => ({
}
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
test: /\.s?css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}
]
},

File diff suppressed because it is too large Load diff

View file

@ -19,18 +19,19 @@ defmodule Augie.Application do
AugieWeb.Endpoint,
# Starts a worker by calling: Augie.Worker.start_link(arg)
# {Augie.Worker, arg},
Augie.UpdateLockManager,
Augie.DistributedSupervisor,
Augie.Collector.RaspberryPi
Augie.UpdateLockManager,
Augie.Collector.RaspberryPi,
Augie.Collector.ErlangSystem,
Augie.Collector.ErlangMemory
]
children =
if enable_mdns?(),
do:
children ++
[
do: [
Augie.MdnsInterfaceWatcher,
{Cluster.Supervisor, [topologies, [name: Augie.ClusterSupervisor]]}
| children
],
else: children

View file

@ -24,6 +24,17 @@ defmodule Augie.Collector do
# config :augie, Augie.Collector, sample_gc_freq: 1000
@default_sample_gc_freq 1_000
@doc """
Check if the collector is up and running - sometimes it takes a while to start
due to Swarm syncs.
"""
@spec alive?() :: bool
def alive? do
__MODULE__
|> Swarm.whereis_name()
|> is_pid()
end
@doc """
Store a record (or records) for a given device at the current time. Devices and metrics are
created dynamically.
@ -107,28 +118,52 @@ defmodule Augie.Collector do
containing a five-element tuple of `:metric`, the device identifier, the
sample name, the sample value and the sample time.
"""
@spec subscribe(device_id) :: :ok | {:error, any}
def subscribe(device_id), do: Swarm.join({__MODULE__, :subscribers, device_id}, self())
@spec subscribe_to_device(device_id) :: :ok | {:error, any}
def subscribe_to_device(device_id) do
with :ok <- :pg2.create({__MODULE__, :subscribers, device_id}),
:ok <- :pg2.join({__MODULE__, :subscribers, device_id}, self()),
do: :ok
end
@doc """
Subscribe to a specifi metric from a specific device.
"""
@spec subscribe(device_id, metric_name) :: :ok | {:error, any}
def subscribe(device_id, metric_name),
do: Swarm.join({__MODULE__, :subscribers, device_id, metric_name}, self())
@spec subscribe_to_metric(device_id, metric_name) :: :ok | {:error, any}
def subscribe_to_metric(device_id, metric_name) do
with :ok <- :pg2.create({__MODULE__, :subscribers, device_id, metric_name}),
:ok <- :pg2.join({__MODULE__, :subscribers, device_id, metric_name}, self()),
do: :ok
end
@doc """
Subscribe to updates about new devices.
"""
@spec subscribe_to_new_devices() :: :ok | {:error, any}
def subscribe_to_new_devices do
with :ok <- :pg2.create({__MODULE__, :new_device_subscribers}),
:ok <- :pg2.join({__MODULE__, :new_device_subscribers}, self()),
do: :ok
end
@doc """
Unsubscribe the caller from the device.
"""
@spec unsubscribe(device_id) :: :ok | {:error, any}
def unsubscribe(device_id), do: Swarm.leave({__MODULE__, :subscribers, device_id}, self())
@spec unsubscribe_from_device(device_id) :: :ok | {:error, any}
def unsubscribe_from_device(device_id),
do: :pg2.leave({__MODULE__, :subscribers, device_id}, self())
@doc """
Unsubscribe the caller from a specific metric.
"""
@spec unsubscribe(device_id, metric_name) :: :ok | {:error, any}
def unsubscribe(device_id, metric_name),
do: Swarm.leave({__MODULE__, :subscribers, device_id, metric_name}, self())
@spec unsubscribe_from_metric(device_id, metric_name) :: :ok | {:error, any}
def unsubscribe_from_metric(device_id, metric_name),
do: :pg2.leave({__MODULE__, :subscribers, device_id, metric_name}, self())
@doc """
Unsubscribe the caller from new device events.
"""
@spec unsubscribe_from_new_devices :: :ok | {:error, any}
def unsubscribe_from_new_devices, do: :pg2.leave({__MODULE__, :new_device_subscribers}, self())
@doc false
def child_spec(_) do
@ -158,7 +193,7 @@ defmodule Augie.Collector do
{:ok, timer} = :timer.send_interval(sample_gc_freq, :gc)
{:ok, %{table: table, sample_ttl: sample_ttl, timer: timer}}
{:ok, %{table: table, sample_ttl: sample_ttl, timer: timer, devices: []}}
end
@impl true
@ -168,42 +203,46 @@ defmodule Augie.Collector do
end
@impl true
def handle_cast({:record, device_id, values, sampled_at}, %{table: table} = state) do
def handle_cast(
{:record, device_id, values, sampled_at},
%{table: table, devices: devices} = state
) do
sampled_at_us = sampled_at |> DateTime.to_unix(:microsecond)
rows =
values
|> Enum.map(fn {name, value} ->
# I know, I know. Side-effects in a map.
Swarm.publish(
{__MODULE__, :subscribers, device_id},
{:metric, device_id, name, value, sampled_at}
)
Swarm.publish(
{__MODULE__, :subscribers, name, device_id},
{:metric, device_id, name, value, sampled_at}
)
broadcast_metric(device_id, name, value, sampled_at)
{device_id, :metric, name, value, sampled_at_us}
end)
:ets.insert(table, rows)
if Enum.member?(devices, device_id) do
{:noreply, state}
else
broadcast_new_device(device_id)
{:noreply, %{state | devices: [device_id | devices]}}
end
end
def handle_cast({:metadata, device_id, metadata}, %{table: table} = state) do
def handle_cast({:metadata, device_id, metadata}, %{table: table, devices: devices} = state) do
rows =
metadata
|> Enum.map(fn {name, value} ->
Swarm.publish({__MODULE__, :subscribers, device_id}, {:metadata, device_id, name, value})
broadcast_metadata(device_id, name, value)
{device_id, :metadata, name, value}
end)
:ets.insert(table, rows)
if Enum.member?(devices, device_id) do
{:noreply, state}
else
broadcast_new_device(device_id)
{:noreply, %{state | devices: [device_id | devices]}}
end
end
@impl true
@ -292,6 +331,7 @@ defmodule Augie.Collector do
{last, min, max, avg} =
samples
|> Map.keys()
|> Enum.sort()
|> Enum.reduce({[], [], [], []}, fn name, {last, min, max, avg} ->
samples_for_this_metric =
samples
@ -319,4 +359,52 @@ defmodule Augie.Collector do
{:noreply, state}
end
defp broadcast_metric(device_id, metric, sample, sampled_at) do
device_members =
case :pg2.get_members({__MODULE__, :subscribers, device_id}) do
members when is_list(members) -> members
{:error, {:no_such_group, _}} -> []
end
metric_members =
case :pg2.get_members({__MODULE__, :subscribers, device_id, metric}) do
members when is_list(members) -> members
{:error, {:no_such_group, _}} -> []
end
device_members
|> Enum.concat(metric_members)
|> Enum.sort()
|> Enum.uniq()
|> Enum.each(&send(&1, {:metric, device_id, metric, sample, sampled_at}))
:ok
end
defp broadcast_metadata(device_id, name, value) do
case :pg2.get_members({__MODULE__, :subscribers, device_id}) do
members when is_list(members) ->
members
|> Enum.each(&send(&1, {:metadata, device_id, name, value}))
:ok
{:error, {:no_such_group, _}} ->
:ok
end
end
defp broadcast_new_device(device_id) do
case :pg2.get_members({__MODULE__, :new_device_subscribers}) do
members when is_list(members) ->
members
|> Enum.each(&send(&1, {:new_device, device_id}))
:ok
{:error, {:no_such_group, _}} ->
:ok
end
end
end

View file

@ -72,6 +72,6 @@ defmodule Augie.Collector.Cluster do
end
defp update_metadata(nodes) do
Collector.metadata(__MODULE__, nodes: nodes, collector: __MODULE__)
Collector.metadata(__MODULE__, nodes: nodes, collector: __MODULE__, name: "Cluster Health")
end
end

View file

@ -0,0 +1,52 @@
defmodule Augie.Collector.ErlangMemory do
alias Augie.Collector
use GenServer
@moduledoc """
This module retrieves information about the Erlang system it is running on.
"""
@update_interval 5_000
@doc false
def start_link(args), do: GenServer.start_link(__MODULE__, [args])
@impl true
def init(_args) do
Collector.metadata({__MODULE__, node()}, metadata())
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
{:ok, %{timer: timer, count: 0}}
end
@impl true
def terminate(_reason, %{timer: timer}) do
:timer.cancel(timer)
:ok
end
@impl true
def handle_info(:tick, %{count: 3} = state) do
Collector.record({__MODULE__, node()}, collect_stats())
Collector.metadata({__MODULE__, node()}, metadata())
{:noreply, state}
end
def handle_info(:tick, %{count: count} = state) do
Collector.record({__MODULE__, node()}, collect_stats())
{:noreply, %{state | count: count + 1}}
end
defp collect_stats() do
ExErlstats.memory()
|> Enum.into([])
|> Enum.sort_by(&elem(&1, 0))
end
defp metadata() do
[
name: "Erlang Memory",
collector: __MODULE__,
node: node()
]
end
end

View file

@ -0,0 +1,57 @@
defmodule Augie.Collector.ErlangSystem do
alias Augie.Collector
use GenServer
@moduledoc """
This module retrieves information about the Erlang system it is running on.
"""
@update_interval 5_000
@doc false
def start_link(args), do: GenServer.start_link(__MODULE__, [args])
@impl true
def init(_args) do
Collector.metadata({__MODULE__, node()}, metadata())
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
{:ok, %{timer: timer, count: 0}}
end
@impl true
def terminate(_reason, %{timer: timer}) do
:timer.cancel(timer)
:ok
end
@impl true
def handle_info(:tick, %{count: 3} = state) do
Collector.record({__MODULE__, node()}, collect_stats())
Collector.metadata({__MODULE__, node()}, metadata())
{:noreply, state}
end
def handle_info(:tick, %{count: count} = state) do
Collector.record({__MODULE__, node()}, collect_stats())
{:noreply, %{state | count: count + 1}}
end
defp collect_stats() do
sys_info =
ExErlstats.system_info()
|> Map.take(~w[port_count process_count schedulers_online]a)
ExErlstats.stats()
|> Map.take(~w[run_queue, total_active_tasks total_run_queue_lengths]a)
|> Map.merge(sys_info)
|> Enum.into([])
|> Enum.sort_by(&elem(&1, 0))
end
defp metadata() do
ExErlstats.system_info()
|> Map.take(~w[otp_release port_limit process_limit schedulers version]a)
|> Map.merge(%{name: "Erlang System", collector: __MODULE__, node: node()})
|> Enum.into([])
end
end

View file

@ -7,7 +7,7 @@ defmodule Augie.Collector.RaspberryPi do
the collector. Mostly using the `vcgencmd` utility.
"""
@update_frequency 5_000
@update_interval 5_000
@vcgencmd_stats [
temperature: "measure_temp",
arm_clock: "measure_clock arm",
@ -17,25 +17,14 @@ defmodule Augie.Collector.RaspberryPi do
sdram_p_voltage: "measure_volts sdram_p"
]
@doc false
def child_spec(_) do
%{
id: {__MODULE__, hostname()},
start: {GenServer, :start_link, [__MODULE__, []]},
restart: :permanent,
type: :worker
}
end
@doc false
def start_link(args), do: GenServer.start_link(__MODULE__, [args])
@impl true
def init(_args) do
hostname = hostname()
Collector.metadata({__MODULE__, hostname}, metadata())
{:ok, timer} = :timer.send_interval(@update_frequency, :tick)
{:ok, %{hostname: hostname, timer: timer}}
Collector.metadata({__MODULE__, node()}, metadata())
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
{:ok, %{timer: timer, count: 0}}
end
@impl true
@ -45,11 +34,17 @@ defmodule Augie.Collector.RaspberryPi do
end
@impl true
def handle_info(:tick, %{hostname: hostname} = state) do
Collector.record({__MODULE__, hostname}, collect_stats())
def handle_info(:tick, %{count: 3} = state) do
Collector.record({__MODULE__, node()}, collect_stats())
Collector.metadata({__MODULE__, node()}, metadata())
{:noreply, state}
end
def handle_info(:tick, %{count: count} = state) do
Collector.record({__MODULE__, node()}, collect_stats())
{:noreply, %{state | count: count + 1}}
end
defp collect_stats do
@vcgencmd_stats
|> Enum.reduce([], fn {name, command}, stats ->
@ -92,7 +87,8 @@ defmodule Augie.Collector.RaspberryPi do
model: read_file("/sys/firmware/devicetree/base/model", "Unknown model"),
serial_number: read_file("/sys/firmware/devicetree/base/serial-number"),
memory: total_memory(),
collector: __MODULE__
collector: __MODULE__,
name: "Raspberry Pi Status"
]
end

View file

@ -13,7 +13,7 @@ defmodule Augie.MdnsClusterStrategy do
@service_domain :"_erlang._tcp.local"
@host_domain "_ppp.local"
@update_frequency 10_000
@update_interval 10_000
@doc false
def start_link(args), do: GenServer.start_link(__MODULE__, args)
@ -24,7 +24,7 @@ defmodule Augie.MdnsClusterStrategy do
:ok = Mdns.EventManager.register()
:timer.sleep(100)
advertise_services()
{:ok, timer} = :timer.send_interval(@update_frequency, :tick)
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
send(self(), :tick)
{:ok, %{seen: [], state: state, timer: timer}}
end

View file

@ -17,14 +17,14 @@ defmodule Augie.MdnsInterfaceWatcher do
"""
@default_interface "eth0"
@update_frequency 10_000
@update_interval 10_000
def start_link(_), do: GenServer.start_link(__MODULE__, [])
@impl true
def init(_) do
interface = System.get_env("MDNS_INTERFACE", @default_interface)
{:ok, timer} = :timer.send_interval(@update_frequency, :tick)
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
send(self(), :tick)
{:ok, %{interface: interface, timer: timer, ip_address: nil, server_running: false}}
end

View file

@ -23,22 +23,16 @@ defmodule Augie.UpdateLockManager do
@lockfile_location "/tmp/balena/updates.lock"
# How frequently to message the other peer(s) and decide whom should be the lock holder.
@update_frequency 10_000
@update_interval 10_000
@doc false
def start_link(args) do
with {:ok, pid} <- GenServer.start_link(__MODULE__, [args]),
:yes <- Swarm.register_name({__MODULE__, node()}, pid),
:ok <- Swarm.join(__MODULE__, pid) do
{:ok, pid}
end
end
def start_link(args), do: GenServer.start_link(__MODULE__, [args])
@impl true
def init(_args) do
if File.exists?(@lockfile_location), do: File.rmdir(@lockfile_location)
Process.flag(:trap_exit, true)
{:ok, timer} = :timer.send_interval(@update_frequency, :tick)
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
now = DateTime.utc_now()
{:ok, %State{timer: timer, started_at: now}}
end
@ -66,7 +60,7 @@ defmodule Augie.UpdateLockManager do
|> Enum.reject(fn {_, %{last_seen: d}} ->
now = DateTime.utc_now() |> DateTime.to_unix()
last_seen_at = d |> DateTime.to_unix()
now - last_seen_at > @update_frequency * 3
now - last_seen_at > @update_interval * 3
end)
|> Enum.into(%{})

View file

@ -0,0 +1,56 @@
defmodule AugieWeb.DashboardLive do
use Phoenix.LiveView
alias Augie.Collector
def render(assigns) do
~L"""
<%= if @collector_alive do %>
<div class="grid-x grid-padding-x grid-padding-y small-up-1 medium-up-2 large-up-3">
<%= for device <- @devices do %>
<div class="cell">
<%= live_render(@socket, AugieWeb.DeviceMetricLive, id: Base.encode64(:erlang.term_to_binary(device)), session: %{id: device}) %>
</div>
<% end %>
</div>
<% else %>
<div class="callout alert">
<h2>Collector not running</h2>
<p>
The statistics collector is not running, so there are not stats to show you. This probably means that the machine is still coming up.
</p>
</div>
<% end %>
"""
end
def mount(_, socket) do
if Collector.alive?() do
if connected?(socket), do: Collector.subscribe_to_new_devices()
{:ok, devices} = Collector.devices()
{:ok, assign(socket, devices: device_sorter(devices), collector_alive: true)}
else
if connected?(socket), do: Process.send_after(self(), :collector_check, 250)
{:ok, assign(socket, devices: [], collector_alive: false)}
end
end
def handle_info(:collector_check, socket) do
{:ok, socket} = mount(nil, socket)
{:noreply, socket}
end
def handle_info({:new_device, device_id}, socket) do
devices = [device_id | socket.assigns.devices]
{:noreply, assign(socket, devices: device_sorter(devices))}
end
defp device_sorter(devices) do
devices
|> Enum.sort_by(fn
{module, id} when is_atom(module) -> {module, id}
module when is_atom(module) -> {module, 0}
end)
end
end

View file

@ -0,0 +1,79 @@
defmodule AugieWeb.DeviceMetricLive do
use Phoenix.LiveView
alias Augie.Collector
def render(assigns) do
~L"""
<div class="card">
<div class="card-divider">
<h4><%= Keyword.get(@metadata, :name, "Unknown Device") %></h4>
</div>
<%= if Enum.any?(@metadata) do %>
<table class="table card-section unstriped stack">
<thead>
<tr>
<td colspan="2">Metadata</td>
</tr>
</thead>
<tbody>
<%= for {name, value} <- Keyword.drop(@metadata, ~w[name collector]a) do %>
<tr>
<td><%= to_string name %></td>
<td>
<%= if is_list(value) do %>
<%= Enum.join(value, ", ") %>
<% else %>
<%= to_string value %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<%= if Enum.any?(@last) do %>
<table class="table card-section unstriped stack">
<thead>
<tr>
<td colspan="2">Metrics</td>
</tr>
</thead>
<tbody>
<%= for {name, {sample, _}} <- @last do %>
<tr>
<td><%= to_string name %></td>
<td><%= sample %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
</div>
"""
end
def mount(%{id: device_id}, socket) do
if connected?(socket), do: Collector.subscribe_to_device(device_id)
{:ok, status} = Collector.status(device_id)
{:ok, assign(socket, Enum.into(status, %{}))}
end
def handle_info({:metric, _key, metric_name, value, sample_time}, socket) do
last =
socket.assigns.last
|> Keyword.put(metric_name, {value, sample_time})
|> Enum.sort_by(&elem(&1, 0))
{:noreply, assign(socket, %{last: last})}
end
def handle_info({:metadata, _key, name, value}, socket) do
metadata =
socket.assigns.metadata
|> Keyword.put(name, value)
|> Enum.sort_by(&elem(&1, 0))
{:noreply, assign(socket, %{metadata: metadata})}
end
end

View file

@ -18,6 +18,7 @@ defmodule AugieWeb.Router do
pipe_through(:browser)
get("/", PageController, :index)
live("/dashboard", DashboardLive)
end
# Other scopes may use custom stacks.

View file

@ -1,30 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Augie · Phoenix Framework</title>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Augie the hexapod robot</title>
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
</head>
<body>
<header>
<section class="container">
<nav role="navigation">
<ul>
<li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
<div class="top-bar">
<div class="top-bar-left">
<ul class="dropdown menu" data-dropdown-menu>
<li class="menu-text">Augie <span class="label"><%= app_version() %></span></li>
<li>
<%= link "Dashboard", to: Routes.live_path(@conn, AugieWeb.DashboardLive) %>
</li>
</ul>
</nav>
<a href="http://phoenixframework.org/" class="phx-logo">
<img src="<%= Routes.static_path(@conn, "/images/phoenix.png") %>" alt="Phoenix Framework Logo"/>
</a>
</section>
</header>
<main role="main" class="container">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
</div>
</div>
<div class="grid-container">
<div class="grid-x grid-margin-x">
<div class="cell">
<%= if get_flash(@conn, :info) do %>
<div class="callout warning">
<%= get_flash(@conn, :info) %>
</div>
<% end %>
<%= if get_flash(@conn, :error) do %>
<div class="callout alert">
<%= get_flash(@conn, :error) %>
</div>
<% end %>
</div>
</div>
<%= render @view_module, @view_template, assigns %>
</main>
</div>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</body>
</html>

View file

@ -1,35 +0,0 @@
<section class="phx-hero">
<h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
<p>A productive web framework that<br/>does not compromise speed or maintainability.</p>
</section>
<section class="row">
<article class="column">
<h2>Resources</h2>
<ul>
<li>
<a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix">Source</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix/blob/v1.4/CHANGELOG.md">v1.4 Changelog</a>
</li>
</ul>
</article>
<article class="column">
<h2>Help</h2>
<ul>
<li>
<a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
</li>
<li>
<a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
</li>
<li>
<a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
</li>
</ul>
</article>
</section>

View file

@ -1,3 +1,7 @@
defmodule AugieWeb.LayoutView do
use AugieWeb, :view
def app_version() do
Application.spec(:augie, :vsn)
end
end

View file

@ -32,21 +32,22 @@ defmodule Augie.MixProject do
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.4.11"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:balena_device, "~> 0.1"},
{:credo, "~> 1.1", only: [:dev, :test], runtime: false},
{:ex_erlstats, "~> 0.1"},
{:floki, ">= 0.0.0", only: :test},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:balena_device, "~> 0.1"},
{:mdns, "~> 1.0"},
{:swarm, "~> 3.4"},
{:swarm_dynamic_supervisor, "~> 0.1.0"},
{:libcluster, "~> 3.1"},
{:mdns, "~> 1.0"},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_view, "~> 0.4.0"},
{:floki, ">= 0.0.0", only: :test},
{:credo, "~> 1.1", only: [:dev, :test], runtime: false}
{:phoenix_pubsub, "~> 1.1"},
{:phoenix, "~> 1.4.11"},
{:plug_cowboy, "~> 2.0"},
{:swarm_dynamic_supervisor, "~> 0.1.0"},
{:swarm, "~> 3.4"}
]
end
end

View file

@ -7,6 +7,7 @@
"credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"dbus": {:hex, :dbus, "0.7.0", "71b7660523be6e222a8a4f9ddcbf54ad95638479bd17654975bb751aadbe81de", [:make, :rebar3], [], "hexpm"},
"dns": {:hex, :dns, "2.1.2", "81c46d39f7934f0e73368355126e4266762cf227ba61d5889635d83b2d64a493", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm"},
"ex_erlstats": {:hex, :ex_erlstats, "0.1.6", "f06af6136e67c2ba65848d411f63de8d672f972907e2e357c14ab3d067a6269c", [:mix], [], "hexpm"},
"file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"},
"floki": {:hex, :floki, "0.23.1", "e100306ce7d8841d70a559748e5091542e2cfc67ffb3ade92b89a8435034dab1", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"},
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},

Binary file not shown.