お手軽プレゼンス管理、リトライ! その5 (サンプルのページと運用)

今回も前回からの続きです。

今回は前回までで作成したAPIを使ったWebページを作ってみます。
あんまり凝ったのは作れないのでテーブルだけでシンプルに作ります。

f:id:hollyhockberry:20210905164846p:plain

静的ファイルのマウント

静的ファイル - FastAPI で静的ファイルのマウントができるそうなのでこ横着してそこにページを作ってしまいます。

チュートリアルによると aiofilesが必要らしいのでインストールします。

pip3 install aiofiles

webapiの下にsampleというディレクトリを作ってHello worldとだけ書いたindex.htmlを作成します。

そして、api.py を以下のように書き換えて実行してみます。

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

app.mount(
  "/api/sample",
  StaticFiles(directory="sample", html=True),
  name="spa")

if __name__ == "__main__":
  import uvicorn
  uvicorn.run("api:app", host="0.0.0.0", port=1234, reload=True)

http://*******:1234/api/sample をブラウザで開くと、めでたくHello worldされてますね。

f:id:hollyhockberry:20210905171053p:plain

ページの構築

では簡単に検索用ページの構築をしていきましょう。

今回はJavascriptフレームワークVue.jsを、CSSフレームワークBootStrapVueを利用します。 非同期のHTTP通信を行いたいので、axiosも利用します。

index.html

<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link type="text/css" rel="stylesheet" href="https://unpkg.com/bootstrap@4.5.3/dist/css/bootstrap.min.css" >
  <link type="text/css" rel="stylesheet" href="https://unpkg.com/bootstrap-vue@2.21.2/dist/bootstrap-vue.min.css" >
</head>
<body>
  <div id="app">
    <b-button @click="search">search</b-button>
    <div>
      <b-table :items="result" :fields="fields" striped hover small />
    </div>
  </div>
  <script src="https://unpkg.com/vue@2.6.12/dist/vue.min.js"></script>
  <script src="https://unpkg.com/axios@0.21.1/dist/axios.min.js"></script>
  <script src="https://unpkg.com/bootstrap-vue@2.21.2/dist/bootstrap-vue.min.js"></script>
  <script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
  <script type="text/javascript" src="./main.js"></script>
</body>
</html>

main.js

function convert(d) {
  var formattedDate = function(date) {
        return date.getFullYear()
          + '/' + ('0' + (date.getMonth() + 1)).slice(-2)
          + '/' + ('0' + date.getDate()).slice(-2)
          + ' ' + ('0' + date.getHours()).slice(-2)
          + ':' + ('0' + date.getMinutes()).slice(-2)
          + ':' + ('0' + date.getSeconds()).slice(-2)
      };
  d.time = formattedDate(new Date(d.time))
  return d
}

function listed(data) {
  data.map(d => convert(d))
  return data
}

var app = new Vue({
  el: '#app',
  data: {
    result: [],
    fields: [
        { key: 'name', sortable: true },
        { key: 'location', sortable: true },
        { key: 'time', sortable: true },
    ]
  },
  methods: {
    search: function() {
      axios
        .get(`http://********:1234/api/search/`)
        .then(response => {
          this.result = listed(response.data)
        })
        .catch(error => {
          this.result = []
        })
    }
  }
});

ボタンを押してsearchメソッドがコールされると、http://********:1234/api/search/ を呼び出します。
その結果をListにして this.resultに代入します。

resultは b-table の itemsにバインドされているので、いい感じで表示してくれます。

bootstrap-vue.org

f:id:hollyhockberry:20210905223014p:plain

これらを前回作成した api/searchAPIに合わせた入力を与えることで冒頭のイメージで示したページが出来上がります。

github.com

運用

Webページもできたのでひとまず完成ですが、いちいちコマンドラインで起動するんじゃなくてサービス化しましょう。

まずはWebサーバであるngnixをインストールします。

sudo apt install nginx

ブラウザからhttp://raspberrypi.localを開いて

f:id:hollyhockberry:20210905213852p:plain

このページが表示されればセットアップできています。

次はASGIです。
すでにuvicornを利用してるのでそれでいいんですが、Uvicornのドキュメント

Run gunicorn -k uvicorn.workers.UvicornWorker for production.

との記載があるので、それに従います。

インストールはpipで行います。

pip3 install gunicorn

設定はgunicorn.conf.pyに書くっぽいのでファイルを用意します。

from os import getenv
from multiprocessing import cpu_count

bind = '0.0.0.0:' + str(getenv('PORT', 8000))
worker_class = 'uvicorn.workers.UvicornWorker'
workers = cpu_count()

loglevel = "debug"
accesslog = None

続いてWebAppをサービス化します。

/etc/systemd/systemに*.serviceファイルを作成します(シンボリックリンクの方がいいかもです)。 ファイル名はわかればなんでもいいです。私はtrack-location.serviceとしてみました。

track-location.service

[Unit]
Description = Tracker API daemon

[Service]
User=pi
WorkingDirectory=/home/pi/App/track-location/webapi
ExecStart=/home/pi/.local/bin/gunicorn api:app

[Install]
WantedBy=multi-user.target

ユーザ名やパスは各自の環境に合わせて変更してください。

これで準備が整ったのでデーモンを登録します。

sudo systemctrl daemon-reload
sudo systemctrl start track-location.service

問題なく起動できているかは systemctrl status track-location.service で確認できます。

f:id:hollyhockberry:20210905220559p:plain

うまく起動できていれば、nginxに繋げてしまえば完了です。

nginxの設定を記述します。

/etc/nginx/conf.d/default.conf

server {
  listen 80 default;
  server_name 127.0.0.1 localhost;

  location /api {
    proxy_pass http://127.0.0.1:8000/api;
  }
}

また最初からある /etc/nginx/sites-enabled/default を削除します。

ここまでできたらnginxを再起動して http://*******/api/sample をブラウザで開いてみましょう。

f:id:hollyhockberry:20210905221334p:plain

作成したアプリがひらけました。

期間を"Today"にしてみると、

f:id:hollyhockberry:20210905221533p:plain

あおいちゃんが学園寮に、いちごちゃんが学園長室にそれぞれ20:48頃までいたことが確認できます。
その後の位置は確認できてないので、きっと二人でどこかに出かけたんでしょうね。

と、動作確認で適当に登録したデータでのストーリー妄想は置いておいて、ゆるーくお手軽にプレゼンス管理するソリューションが出来上がりました。

思った以上に説明が長引いてしまったのが反省点ですが、なかなか楽しかったです。

追記

実験的に数人の有志に協力してもらって運用してみました。
意外なことにそこそこ使い物になったんですが、帰宅時にPCを閉じるだけでスリープもさせない運用の人が少なからずいて、24時間在室しっぱなしでよくわからんという状況が発生してました😅
また、実験などでしばらくPC触らない人も結構いて、同じ職場でもいろんな働き方の人がいるんだなって再確認させられました。

色々考えたけど、まだまだ工夫が必要だったな、、というオチでした。
頭の中だけで考えてると仕様の抜けが簡単に発生しますね(反省)